Оригинальная статья: Rocio Aramberri — Optimizing Django ORM Queries
Django ORM (Object Relational Mapping) — одна из самых мощных функций Django. Благодаря ей мы можем взаимодействовать с базой данных, используя код Python вместо SQL.
Спонсор поста Раскрутка сайта в Краснодаре — Веб-студия KubiQ:
Веб-студия Kubiq.studio
предлагает услугу раскрутки сайтов в Краснодаре.
Ответственность при выполнении задач — наш главный козырь.
В наше время у каждой успешной компании есть свой собственный интернет-ресурс.
Но просто создать его недостаточно. Необходимо что бы о нем узнало как можно
больше людей. А для этого необходимо провести не менее
трудоемкую и сложную работу по раскрутке ресурса.
Только в этом случае от него будет какая та польза и с помощью него компания сможет
составить конкуренцию другим игрокам рынка.
Использование ORM дает несколько преимуществ:
Но также у ORM есть некоторые недостатки:
Чтобы преодолеть недостатки, нужно поближе познакомиться с ORM и понять как она работает под капотом.
Во-первых, нам необходимо понять, что происходит в нашей системе, какие SQL запросы выполняются и какие из них наиболее сильно нагружают систему.
Вот несколько механизмов проверки запросов SQL по мере их выполнения:
Когда установлен debug = True, можно получить доступ к запросам, которые были выполнены, через connection.queries:
>>> from django.db import connection >>> Post.objects.all() >>> connection.queries [ { 'sql': 'SELECT "blogposts_post"."id", "blogposts_post"."title", ' '"blogposts_post"."content", "blogposts_post"."blog_id", ' '"blogposts_post"."published" FROM "blogposts_post" LIMIT 21', 'time': '0.000' } ]
connection.queries содержит список SQL-запросов в виде словарей, содержащих код SQL и время выполнения.
Со времени список запросов может легко вырасти в большой словарь с которым будет сложно разобраться. Чтобы это исправить, Django предоставляет способ очистки:
>>> from django.db import reset_queries >>> reset_queries()
Существует хорошая библиотека django-extensions, которая имеет несколько полезных функций.
Одна из них shell_plus. Это оболочка Django с дополнительными расширениями. Если вы вызываете ее с параметром —print-sql, то будут отображаться SQL-запросы по мере их выполнения при запуске вашего кода.
Я буду использовать shell_plus в этом посте, чтобы вы могли видеть запросы SQL, которые выполняются по мере выполнения кода.
Вот быстрый пример того, как будет выглядеть вывод:
$ ./manage.py shell_plus --print-sql >>> post = Post.objects.get(id=1) SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1
Так же существует инструмент профилирования django-silk (https://github.com/jazzband/django-silk). Он перехватывает запросы, записывает выполненные SQL-запросы и предоставляет способ их визуализации.
Вы сможете просматривать запросы, просматривать список выполненных SQL-запросов и смотреть подробную информацию о конкретном запросе, в том числе о том, какая строка кода вызвала выполнение определенного запроса.
Библиотека django-debug-toolbar (https://github.com/jazzband/django-debug-toolbar) добавляет панель инструментов в ваш браузер, которая покажет вам много отладочной информации во время работы вашего проекта. Используя его, вы можете увидеть количество запросов SQL, которые были выполнены. Также возможно проверить эти запросы, проверить код SQL и посмотреть, в каком порядке они выполнялись и сколько времени занимал каждый из них.
Мы будем использовать следующие модели в качестве примера:
class Blog(models.Model): name = models.CharField(max_length=250) url = models.URLField() def __str__(self): return self.name class Author(models.Model): name = models.CharField(max_length=250) email = models.EmailField() def __str__(self): return self.name class Post(models.Model): title = models.CharField(max_length=250) content = models.TextField() published = models.BooleanField(default=False) blog = models.ForeignKey(Blog, on_delete=models.CASCADE) authors = models.ManyToManyField(Author, related_name="posts") def __str__(self): return self.title
Если нам просто нужно получить id поля ForeignKey, мы можем использовать кэшированный id, который Django уже кешировал для нас, через <field_name>_id.
Давайте посмотрим на это на примере:
>>> Post.objects.first().blog.id SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1 Execution time: 0.001668s [Database: default] SELECT "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_blog" WHERE "blogposts_blog"."id" = 1 LIMIT 21 Execution time: 0.000197s [Database: default]
При обращении к id блога через вложенный объект blog будет сгенерирован новый запрос SQL для получения всего объекта blog. Но так как нам не потребуется доступ к какому-либо другому атрибуту из объекта blog, мы можем полностью избежать выполнения вышеуказанного запроса, выполнив:
>>> Post.objects.first().blog_id SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1 Execution time: 0.000165s [Database: default]
Django не может предвидеть, когда нам потребуется доступ к отношениям ForeignKey из модели, к которой мы обращаемся. Функция select_related позволяет нам точно указать Django, какие связанные модели мы хотим, чтобы он мог заранее выполнить JOINs.
В нашем примере у нас есть модель Post. Post принадлежит определенному Blog. Это отношение представлено в базе данных через ForeignKey из Post в Blog.
Чтобы получить доступ к определенному объекту Post, мы можем сделать следующее:
>>> post = Post.objects.get(id=1) SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" ORDER BY "blogposts_post"."id" ASC LIMIT 1
Если мы хотим получить доступ к объекту Blog из Post, мы можем сделать так:
>>> post.blog SELECT "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_blog" WHERE "blogposts_blog"."id" = 1 LIMIT 21 Execution time: 0.000602s [Database: default] <Blog: Rocio's Blog>
Однако этот оператор сгенерировал новый запрос, чтобы получить информацию из blog. Но этого лучше избегать, особенно есть нужно будет обработать сотни объектов post. Для этого есть select_related. Чтобы использовать его, нам нужно обновить наш оригинальный запрос:
>>> post = Post.objects.select_related("blog").get(id=1) SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published", "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_post" INNER JOIN "blogposts_blog" ON ("blogposts_post"."blog_id" = "blogposts_blog"."id") WHERE "blogposts_post"."id" = 1 LIMIT 21 Execution time: 0.000150s [Database: default]
Обратите внимание, как Django использовал JOIN в SQL запросе выше, чтобы также получить атрибуты из таблицы blog. Теперь при доступе к объекту Blog из Post ему не потребуется дополнительный запрос, поскольку он уже будет кэширован:
>>> post.blog <Blog: Rocio's Blog>
select_related также работает для наборов запросов. Мы можем предварительно выбрать объект blog для всего набора запросов. Если было 50 сообщений и мы не использовали select_related для предварительного выбора объекта блога, потребовалось бы 50 запросов Django для запуска следующего кода. С select_related он просто берет один:
>>> posts = Post.objects.select_related("blog").all() >>> for post in posts: post.blog SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published", "blogposts_blog"."id", "blogposts_blog"."name", "blogposts_blog"."url" FROM "blogposts_post" INNER JOIN "blogposts_blog" ON ("blogposts_post"."blog_id" = "blogposts_blog"."id") Execution time: 0.000224s [Database: default]
prefetch_related похож на select_related, но он используется для предварительного выбора полей ManyToMany. prefetch_related работает по-другому, давайте рассмотрим это на примере.
Допустим, мы хотим получить все post, а затем распечатать авторов для каждого из сообщений. Для этого мы можем сделать следующее:
>>> for post in Post.objects.all(): post.authors.all() SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" Execution time: 0.000158s [Database: default] <QuerySet []> SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" INNER JOIN "blogposts_post_authors" ON ("blogposts_author"."id" = "blogposts_post_authors"."author_id") WHERE "blogposts_post_authors"."post_id" = 1 LIMIT 21 Execution time: 0.000101s [Database: default] <QuerySet []> SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" INNER JOIN "blogposts_post_authors" ON ("blogposts_author"."id" = "blogposts_post_authors"."author_id") WHERE "blogposts_post_authors"."post_id" = 2 LIMIT 21 Execution time: 0.001043s [Database: default] Execution time: 0.000101s [Database: default] <QuerySet []> SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" INNER JOIN "blogposts_post_authors" ON ("blogposts_author"."id" = "blogposts_post_authors"."author_id") WHERE "blogposts_post_authors"."post_id" = 3 LIMIT 21 Execution time: 0.001043s [Database: default]
Обратите внимание, что приведенный выше код сгенерировал 4 запроса: один для получения сообщений, а затем один запрос для каждого из сообщений для получения авторов (всего было 3 сообщения).
Это знаменитая проблема N + 1. Для получения N сообщений, будет выполнено N + 1 запросов. В этом сценарии у нас есть 3 сообщения, которые создадут 4 запроса. Это не так много, но это может очень легко усложниться, когда мы создадим больше сообщений. С 50 сообщениями этот код будет генерировать 51 запрос.
Чтобы избежать этого, мы можем предварительно выбрать авторов с помощью prefetch_related:
>>> for post in Post.objects.prefetch_related("authors").all(): post.authors.all() SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" Execution time: 0.000158s [Database: default] SELECT ("blogposts_post_authors"."post_id") AS "_prefetch_related_val_post_id", "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" INNER JOIN "blogposts_post_authors" ON ("blogposts_author"."id" = "blogposts_post_authors"."author_id") WHERE "blogposts_post_authors"."post_id" IN (1, 2, 3) Execution time: 0.001043s [Database: default]
Теперь было выполнено только 2 запроса. Когда используется prefetch_related, Django сначала получает все сообщения Post, а затем запускает другой SQL-запрос, который извлекает всех авторов для всех сообщений.
В некоторых сценариях простого синтаксиса prefetch_related недостаточно, чтобы Django не выполнял дополнительные запросы. Для большего контроля предварительной выборки вы можете использовать объект предварительной выборки Prefetch.
В нашем примере базы данных есть модель Post и модель Author. Модель Post связана с моделью Author через поле ManyToMany. Допустим, мы хотели перейти от автора к автору и получить все публикации, которые были опубликованы этим автором:
>>> authors = Author.objects.all() >>> for author in authors: print(author.posts.filter(published=True)) SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" Execution time: 0.000251s [Database: default] SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE ("blogposts_post_authors"."author_id" = 1 AND "blogposts_post"."published" = 1) LIMIT 21 Execution time: 0.000178s [Database: default] <QuerySet [<Post: Optimizing Django ORM Queries>, <Post: Placeholder Post>, <Post: Placeholder Post 2>, <Post: Placeholder Post 3>, <Post: Placeholder Post 4>, <Post: Placeholder Post 6>, <Post: Placeholder Post 7>, <Post: Placeholder Post 8>, <Post: Placeholder Post 9>]> SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE ("blogposts_post_authors"."author_id" = 2 AND "blogposts_post"."published" = 1) LIMIT 21 Execution time: 0.000081s [Database: default] <QuerySet [<Post: Optimizing Django ORM Queries>]>
Как видите, в приведенном выше коде было сгенерировано 3 запроса: 1 для получения всех авторов, а затем 2 запроса для получения сообщений для каждого из авторов.
Что если мы использовали prefetch_related? Кажется, это правильно:
>>> authors = Author.objects.prefetch_related("posts").all() >>> for author in authors: print(author.posts.filter(published=True)) SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" Execution time: 0.000097s [Database: default] SELECT ("blogposts_post_authors"."author_id") AS "_prefetch_related_val_author_id", "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE "blogposts_post_authors"."author_id" IN (1, 2) Execution time: 0.000190s [Database: default] SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE ("blogposts_post_authors"."author_id" = 1 AND "blogposts_post"."published" = 1) LIMIT 21 Execution time: 0.000074s [Database: default] <QuerySet [<Post: Optimizing Django ORM Queries>, <Post: Placeholder Post>, <Post: Placeholder Post 2>, <Post: Placeholder Post 3>, <Post: Placeholder Post 4>, <Post: Placeholder Post 6>, <Post: Placeholder Post 7>, <Post: Placeholder Post 8>, <Post: Placeholder Post 9>]> SELECT "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE ("blogposts_post_authors"."author_id" = 2 AND "blogposts_post"."published" = 1) LIMIT 21 Execution time: 0.000070s [Database: default] <QuerySet [<Post: Optimizing Django ORM Queries>]>
Что только что произошло? Мы использовали prefetch_related, чтобы уменьшить количество запросов, но мы фактически увеличили его на 1.
Это происходит потому, что мы фильтруем сообщения с published = True. Django не может использовать наши кэшированные сообщения, так как они не были отфильтрованы при запросе. Чтобы этого не происходило, мы можем настроить набор запросов, используя объект Prefetch:
>>> authors = Author.objects.prefetch_related( Prefetch( "posts", queryset=Post.objects.filter(published=True), to_attr="published_posts", ) ) >>> for author in authors: print(author.published_posts) SELECT "blogposts_author"."id", "blogposts_author"."name", "blogposts_author"."email" FROM "blogposts_author" Execution time: 0.000129s [Database: default] SELECT ("blogposts_post_authors"."author_id") AS "_prefetch_related_val_author_id", "blogposts_post"."id", "blogposts_post"."title", "blogposts_post"."content", "blogposts_post"."blog_id", "blogposts_post"."published" FROM "blogposts_post" INNER JOIN "blogposts_post_authors" ON ("blogposts_post"."id" = "blogposts_post_authors"."post_id") WHERE ("blogposts_post"."published" = 1 AND "blogposts_post_authors"."author_id" IN (1, 2)) Execution time: 0.000089s [Database: default] [<Post: Optimizing Django ORM Queries>, <Post: Placeholder Post>, <Post: Placeholder Post 2>, <Post: Placeholder Post 3>, <Post: Placeholder Post 4>, <Post: Placeholder Post 6>, <Post: Placeholder Post 7>, <Post: Placeholder Post 8>, <Post: Placeholder Post 9>] [<Post: Optimizing Django ORM Queries>]
Мы использовали объект Prefetch, чтобы сообщить Django:
Когда выполняется author.published_posts, новые запросы не будут создаваться, поскольку все уже будет кэшировано. Независимо от количества авторов в нашей системе, операция всегда будет генерировать 2 SQL запроса.
Работая с Django ORM, очень важно думать о том, что происходит под капотом.
Концепции, которые вы узнали из этого поста в блоге, помогут вам написать более оптимизированные запросы и искать возможные варианты оптимизации при просмотре кода. Однако следует помнить, что вы всегда должны обращать внимание на время, которое занимает запрос до и после оптимизации, чтобы убедиться, что оптимизация работает. Иногда меньшее количество запросов не обязательно означает меньшее время, JOIN также могут быть медленными.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Спасибо за полезную статью, в данный момент я тоже рассматриваю ORM.
Очень полезная и понятная статья, спасибо большое!