Django ORM для начинающих | Оптимизируем запросы



    Django ORM (Object Relational Mapping) является одной из самых мощных особенностей Django. Это позволяет нам взаимодействовать с базой данных, используя код Python, а не SQL.

    Для демонстрации опишу такую модель:

    from django.db import models
    
    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)
    
        def __str__(self):
            return self.name
    
    class Post(models.Model):
        title = models.CharField(max_length=250)
        content = models.TextField()
        published = models.BooleanField(default=True)
        blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
        authors = models.ManyToManyField(Author, related_name="posts")
    

    Я буду использовать django-extentions, чтобы получить полезную информацию с помощью:

    python manage.py shell_plus --print-sql
    

    И так начнем:

    >>> post = Post.objects.all()
    >>> post
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
     LIMIT 21
    Execution time: 0.000172s [Database: default]
    <QuerySet [<Post: Post object (1)>]>
    

    1. Используем ForeignKey значения непосредственно


    >>> Post.objects.first().blog.id
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
     ORDER BY "blog_post"."id" ASC
     LIMIT 1
    Execution time: 0.000225s [Database: default]
    SELECT "blog_blog"."id",
           "blog_blog"."name",
           "blog_blog"."url"
      FROM "blog_blog"
     WHERE "blog_blog"."id" = 1
     LIMIT 21
    Execution time: 0.000144s [Database: default]
    1
    

    А так получаем 1 запрос в БД:

    >>> Post.objects.first().blog_id
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
     ORDER BY "blog_post"."id" ASC
     LIMIT 1
    Execution time: 0.000155s [Database: default]
    1
    

    2. OneToMany Relations


    Если мы используем OneToMany отношения мы используем ForeignKey поля и запрос выглядит примерно так:

    >>> post = Post.objects.get(id=1)
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
     WHERE "blog_post"."id" = 1
     LIMIT 21
    Execution time: 0.000161s [Database: default]
    

    И если мы хотим получить доступ к объекту блога из объекта поста, мы можем сделать:

    >>> post.blog
    SELECT "blog_blog"."id",
           "blog_blog"."name",
           "blog_blog"."url"
      FROM "blog_blog"
     WHERE "blog_blog"."id" = 1
     LIMIT 21
    Execution time: 0.000211s [Database: default]
    <Blog: Django tutorials>
    

    Тем не менее, это вызвало новый запрос, чтобы получить информацию из блога. Так что используйте select_related, чтобы избежать этого. Чтобы использовать его, мы можем обновить наш оригинальный запрос:

    >>> post = Post.objects.select_related("blog").get(id=1)
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id",
           "blog_blog"."id",
           "blog_blog"."name",
           "blog_blog"."url"
      FROM "blog_post"
     INNER JOIN "blog_blog"
        ON ("blog_post"."blog_id" = "blog_blog"."id")
     WHERE "blog_post"."id" = 1
     LIMIT 21
    Execution time: 0.000159s [Database: default]
    

    Обратите внимание, что Django использует JOIN сейчас! И время выполнения запроса меньше, чем раньше. Кроме того, теперь post.blog будет кэширован!

    >>> post.blog
    <Blog: Django tutorials>
    

    select_related так же работает с QurySets:

    >>> posts = Post.objects.select_related("blog").all()
    >>> for post in posts:
    ...     post.blog
    ...
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id",
           "blog_blog"."id",
           "blog_blog"."name",
           "blog_blog"."url"
      FROM "blog_post"
     INNER JOIN "blog_blog"
        ON ("blog_post"."blog_id" = "blog_blog"."id")
    Execution time: 0.000241s [Database: default]
    <Blog: Django tutorials>
    

    3. ManyToMany Relations


    Чтобы получить авторов постов мы используем что-то вроде этого:

    >>> for post in Post.objects.all():
    ...     post.authors.all()
    ...
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
    Execution time: 0.000242s [Database: default]
    SELECT "blog_author"."id",
           "blog_author"."name"
      FROM "blog_author"
     INNER JOIN "blog_post_authors"
        ON ("blog_author"."id" = "blog_post_authors"."author_id")
     WHERE "blog_post_authors"."post_id" = 1
     LIMIT 21
    Execution time: 0.000125s [Database: default]
    <QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>, <Author: Guido van Rossum>]>
    SELECT "blog_author"."id",
           "blog_author"."name"
      FROM "blog_author"
     INNER JOIN "blog_post_authors"
        ON ("blog_author"."id" = "blog_post_authors"."author_id")
     WHERE "blog_post_authors"."post_id" = 2
     LIMIT 21
    Execution time: 0.000109s [Database: default]
    <QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>]>
    

    Похоже, мы получили запрос для каждого объекта поста. По этому, мы должны использовать prefetch_related. Это похоже на select_related но используется с ManyToMany Fields:

    >>> for post in Post.objects.prefetch_related("authors").all():
    ...     post.authors.all()
    ...
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."blog_id"
      FROM "blog_post"
    Execution time: 0.000300s [Database: default]
    SELECT ("blog_post_authors"."post_id") AS "_prefetch_related_val_post_id",
           "blog_author"."id",
           "blog_author"."name"
      FROM "blog_author"
     INNER JOIN "blog_post_authors"
        ON ("blog_author"."id" = "blog_post_authors"."author_id")
     WHERE "blog_post_authors"."post_id" IN (1, 2)
    Execution time: 0.000379s [Database: default]
    <QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>, <Author: Guido van Rossum>]>
    <QuerySet [<Author: Dmytro Parfeniuk>, <Author: Will Vincent>]>
    

    Что только что произошло??? Мы сократили количество запросов с 2 до 1, чтобы получить 2 QuerySet-a!

    4. Prefetch object


    prefetch_related достаточно для большинства случаев, но это не всегда помогает избежать дополнительных запросовю К примеру, если мы используем фильтрацию Django не может использовать наши кэшированные posts, так как они не были отфильтрованы, когда они были запрошены в первом запросе. И мы получим:

    >>> authors = Author.objects.prefetch_related("posts").all()
    >>> for author in authors:
    ...     print(author.posts.filter(published=True))
    ...
    SELECT "blog_author"."id",
           "blog_author"."name"
      FROM "blog_author"
    Execution time: 0.000580s [Database: default]
    SELECT ("blog_post_authors"."author_id") AS "_prefetch_related_val_author_id",
           "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."published",
           "blog_post"."blog_id"
      FROM "blog_post"
     INNER JOIN "blog_post_authors"
        ON ("blog_post"."id" = "blog_post_authors"."post_id")
     WHERE "blog_post_authors"."author_id" IN (1, 2, 3)
    Execution time: 0.000759s [Database: default]
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."published",
           "blog_post"."blog_id"
      FROM "blog_post"
     INNER JOIN "blog_post_authors"
        ON ("blog_post"."id" = "blog_post_authors"."post_id")
     WHERE ("blog_post_authors"."author_id" = 1 AND "blog_post"."published" = 1)
     LIMIT 21
    Execution time: 0.000299s [Database: default]
    <QuerySet [<Post: Post object (1)>, <Post: Post object (2)>]>
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."published",
           "blog_post"."blog_id"
      FROM "blog_post"
     INNER JOIN "blog_post_authors"
        ON ("blog_post"."id" = "blog_post_authors"."post_id")
     WHERE ("blog_post_authors"."author_id" = 2 AND "blog_post"."published" = 1)
     LIMIT 21
    Execution time: 0.000336s [Database: default]
    <QuerySet [<Post: Post object (1)>, <Post: Post object (2)>]>
    SELECT "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."published",
           "blog_post"."blog_id"
      FROM "blog_post"
     INNER JOIN "blog_post_authors"
        ON ("blog_post"."id" = "blog_post_authors"."post_id")
     WHERE ("blog_post_authors"."author_id" = 3 AND "blog_post"."published" = 1)
     LIMIT 21
    Execution time: 0.000412s [Database: default]
    <QuerySet [<Post: Post object (1)>]>
    

    То есть, мы использовали prefetch_related, чтобы уменьшить количество запросов, но мы фактически увеличили его. Чтобы этого избежать, мы можем настроить запрос с помощью объекта 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 "blog_author"."id",
           "blog_author"."name"
      FROM "blog_author"
    Execution time: 0.000183s [Database: default]
    SELECT ("blog_post_authors"."author_id") AS "_prefetch_related_val_author_id",
           "blog_post"."id",
           "blog_post"."title",
           "blog_post"."content",
           "blog_post"."published",
           "blog_post"."blog_id"
      FROM "blog_post"
     INNER JOIN "blog_post_authors"
        ON ("blog_post"."id" = "blog_post_authors"."post_id")
     WHERE ("blog_post"."published" = 1 AND "blog_post_authors"."author_id" IN (1, 2, 3))
    Execution time: 0.000404s [Database: default]
    [<Post: Post object (1)>, <Post: Post object (2)>]
    [<Post: Post object (1)>, <Post: Post object (2)>]
    [<Post: Post object (1)>]
    

    Мы использовали определенный запрос для получения постов через параметр запроса и сохранили отфильтрованные сообщения в новом атрибуте. Как мы видим, теперь у нас есть только 2 запроса в базу данных.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 23

      0

      В п.3 наверное было бы проще сразу всех авторов получить у которых post__id в qs. На практике это обычно быстрее, потому что полностью одним запросом к СУБД делается, пусть и с подзапросом.

        0
        Можете пример привести. Я не до конца понял.
          +1
          posts_qs = Posts.objects.all()
          authors = Authors.objects.filter(posts__id__in=posts_qs)
          #  or
          #  authors = Authors.objects.filter(posts__in=posts_qs)

          Он сделает обычный join или подзапрос — уже и не помню. Но движок бд всяко быстрее отработает + там скорее всего будет пагинатор использоваться, а значит такой вариант больше подойдёт. Ну и список авторов будет без всяких distinct выдавать только уникальные записи.

            0
            Либо я не понимаю гениальности этих решений, либо автор и комментатор чего-то не понимают в джанго:
            Authors.objects.filter(posts__id__isnull=False).distinct() — все авторы
            Authors.objects.filter(posts__title__icontains=«кодзима гений»).delete() — выборка авторов по определенному тексту в заголовке поста.
              0
              Либо я не понимаю гениальности этих решений, либо автор и комментатор чего-то не понимают в джанго...

              Либо вам чуждо понятие примера. Вместо .all() может быть любой фильтр и тогда первый пример сразу не подходит. Если параметров для поста довольно много, то можно получить не очень читабельный код, да и оптимизации это не прибавит — SQL будет либо таким же, либо около того.
              Но если запрос тупо на всех авторов, которые имеют хотя бы один пост, то первый ваш пример самый читабельный. Если нужно отобрать только по содержимому заголовка, то второй тоже неплох. Но если говорить о реальном приложении, то очень редко происходит, что нужно получить данные не с уже имеющегося объекта qs.
              Хотя конечно же нужно было мне написать пояснение к моему ходу мыли, чтобы неокрепшие умы не сломались. И вообще, оптимизировать надо исходя из задачи и т.д.

              0
              Хороший пример)
              Но Вы же понимаете, что это исключительно в качестве примера)
          0
          Спасибо, годно как памятка для начинающих и чеклист для всех в целом.
          +2
          Я буду использовать django-extentions, чтобы получить полезную информацию с помощю с

          Вы же это только для логгирования SQL запросов используете (другого применения в статье не нашёл). В таком случае можно просто настроить логгинг в settings.py, сократить список зависимостей, и не вынуждать неподготовленные умы устанавливать лишнее.


          LOGGING = {
              'loggers': {
                  'django.db': {
                      'level': 'DEBUG',
                      'handlers': ['console', 'file'],
                  },
                  ...
              }
              ...
          }

          Вывод получается почти такой же (за отсутствием форматирования). Тут вам и SQL и Execution time (0.024 в скобках — как раз эта самая цифра):


          >>> from myapp.models import Post
          >>> Post.objects.all()
          DEBUG 2020-05-24 13:52:49,540 django.db.backends utils (0.024) SELECT "myapp_post"."id", "myapp_post"."created_date", "myapp_post"."user_id" FROM "myapp_post" ORDER BY "myapp_post"."created_date" ASC  LIMIT 21; args=()
          <InheritanceQuerySet []>
          >>>

          В остальном не увидел ничего интересного. Вся статья с громким и довольно общим заголовком — как пара абзацев из этой страницы документации: https://docs.djangoproject.com/en/3.0/ref/models/querysets/


          Но кроме этого в документации есть отдельная страничка про способы оптимизации, и уж там гораздо больше интересных приёмов для статьи с таким заголовком: https://docs.djangoproject.com/en/3.0/topics/db/optimization/

            0
            Ну статья непосредственно про запросы. Я и не претендовал на то, чтобы показать какие то возможности этого пакета))) Я так же использую логирование… но я хотел сделать пост максимально сжатым и информативным.
              0
              Так shell_plus помимо прочего даёт автоимпорт и автодополнение, чего на него кидаться то, полезная вещь же.
                0

                В статье про оптимизацию запросов эта информация лишняя, и в некоторых случаях — деструктивная. Статья не одноразовая, и если завтра с этой зависимостью что-то случится (как это бывает с любой лишней зависимостью, не имеющей отношения к основной теме) — то у новичка который сюда придёт будет +1 проблема: как установить django-extensions. А ведь есть решение из коробки, которое в контексте работы с джангой было бы просто изначально надежнее.

                  0
                  то у новичка который сюда придёт будет +1 проблема: как установить django-extensions.

                  1) В статье ссылка на гитхаб, там в первом абзаце pip install… 2) Человек, установивший джангу и решивший почитать про оптимизацию запросов с пакетами как-нибудь справится. 3) Может человек, не знакомый с shell_plus установит его и будет потом пользоваться каждый день и благодарить судьбу и автора за то что он упомянул эту тулзу между строк?)
                    0
                    ахах, это точно)
                    Я думаю что столько критики в адрес постов это уже норма на Хабре)
                      0

                      1) выдрали из контекста. перечитайте, пожалуйста, полностью.
                      2) про это ни слова не было.
                      3) для таких случаев делают подборки, типа таких https://habr.com/ru/post/503624/ или таких https://github.com/wsvincent/awesome-django

                0
                является одной из самых мощных особенностей Django.

                Я бы не стал так однозначно говорить. Это как минимум спорно, многие считают это место откровенно слабым во фреймворке и предпочитают алхимию.

                  0
                  Та я и сам алхимию использую. Но зачастую начинающие используют ОРМ и я думаю, что статья такого рода поможет многим начать с меньшим количеством гавнокода)
                  0
                  О каком кешировании в п.1 идет речь, если вы обращаетесь к полю таблицы в которую делаете запрос (в втором случае)?
                    –1
                    Как по мне на примере показано. Когда мы хотим получить доступ к ID через ForeignKey мы можем использовать кешированный ID, с помощью <field_name>_id
                      +1

                      Не вводите людей в заблуждение — это не кеш. Вы используете поиск по полю для который ограничен внешним ключем (foreign key constraint). Это не обязывает вас использовать соединение (join) с таблицей для которой он предназначен. Чем меньше данных должна обработать СУБД, тем быстрее вы получаете результат.

                  +2
                  Вот же знатные Django-воды на начинающего накинулись. Видно же что зеленый ещё.

                  Теперь по делу:

                  Автор, не поверишь, у тебя str(blog_obj) или str(author_obj) в некоторых случаях будут работать неправльно. Прелагаю тебе найти ошибку самостоятельно.

                  Post.objects.first().blog.id можно сделать не только как Post.objects.first().blog_id а еще и Post.objects.values_list('blog__id', flat=True)[0] как и Post.objects.values_list('blog_id', flat=True)[0] как и Post.objects.first().only('blog__id').blog_id и еще много вариантов, появление каждого в проекте имеет свою причину. Попробуй померить время с only.

                  Кроме этого не факт, что поле связи с блогом будет иметь форму «rel_obj»_id, я бы сказал, что это наиболее часто встречающийся случай и только.

                  Использование django-extensions для queryset explain не нужно, и продолжает использоваться только по причине отвратительной документации Django в целом и отсутствия документации по объекту query в частности. query является обязательным аттрибутом queryset. В моей статье про djangoconf 2019 есть ссылка на видео про ORM, там докладчик про explain рассказывает.

                  Кстати, статья про left join на хабре, что автор этой статьи прокомментировал, появилась только потому, что автор «left join» так и не понял, что сделать любой join возможно, правильно использовав queryset.query.

                  Использование «all()» оправдано только в применении к object_manager, т.е. .objects.all() или когда не известно, что придёт в метод — менеджер или queryset.
                  Если любой метод возвращающий queryset применен, то all() излишен.

                  В остальном согласен с комментаторами, в документации информации побольше.

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

                    Самое читаемое