Skip to content

enlike/indexes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

Как можно улучшить производительность Django приложений благодаря построению новых индексов в PostgreSQL

Для начала нужно познакомиться с базой того, как Django ORM конвертирует ваш запрос в SQL:

__iexact

Python:

qs = qs.all()
_iexact = qs.filter(first_name__iexact='олег')
_iexact_sql = _iexact.query.sql_with_params()

SQL:

SELECT * FROM "users_user" WHERE UPPER("users_user"."first_name"::text) = UPPER('олег');

__icontains

Python:

qs = qs.all()
_icontains = qs.filter(first_name__icontains='олег')
_icontains_sql = _icontains.query.sql_with_params()

SQL:

SELECT * FROM "users_user" WHERE UPPER("users_user"."first_name"::text) LIKE UPPER('%олег%');

__exact

Python:

qs = qs.all()
_exact = qs.filter(first_name__exact='Олег')
_exact_sql = _exact.query.sql_with_params()

SQL:

SELECT * FROM "users_user" WHERE "users_user"."first_name" = 'Олег';

__contains

qs = qs.all()
_contains = qs.filter(first_name__contains='Олег')
_contains_sql = _contains.query.sql_with_params()

SQL:

SELECT * FROM "users_user" WHERE "users_user"."first_name"::text LIKE '%Олег%';

Как можно заметить, не всегда очевидно как Django ORM конвертирует в SQL, который выполняется на БД. Особенно это видно в операциях __icontains, __ixact.

Ожидается, что будет использован поиск по ILIKE, но Django использует совсем неочивидный поиск по UPPER.

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

e.g.: в этой статье будет использоваться поле first_name (самый очевидный пример)

Все индексы, что вы создаете по такому сценарию:

first_name = models.CharField(max_length=255, db_index=True)

Создают на бд 2 индекса:

users_user_first_name_7e5e114b -- используется только для операций __exact, __in
users_user_first_name_7e5e114b_like -- индекс вообще не используется, хотя по своей сути должен отвечать за __contains 

Примерный код самих индексов, который выполняет Django в PostgreSQL

CREATE INDEX users_user_first_name_7e5e114b
    ON public.users_user USING btree
    (f_name COLLATE pg_catalog."default" ASC NULLS LAST);
CREATE INDEX users_user_first_name_7e5e114b_like
    ON public.users_user USING btree
    (f_name COLLATE pg_catalog."default" varchar_pattern_ops ASC NULLS LAST);

Когда мы начинаем осуществлять поиск по __icontains, __iexact, __contains эти индексы попросту бесполезны, они не несут в себе того функционала, которое должно покрывать наши запросы

Производительность

Сейчас продемонстрирую интересную разницу в плане запроса С индесами по этим операциям и Без.

Код, который используется, можно увидеть выше

Данные: таблица users_user - 202427 записей настоящих данных пользователей приложения, не foo bar значения

Без индексов

__iexact

"Gather  (cost=1000.00..8454.37 rows=1012 width=721) (actual time=1.090..104.652 rows=926 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on users_user  (cost=0.00..7353.17 rows=422 width=721) (actual time=0.471..72.640 rows=309 loops=3)"
"        Filter: (upper((first_name)::text) = 'ОЛЕГ'::text)"
"        Rows Removed by Filter: 67167"
"Planning time: 2.083 ms"
"Execution time: 117.935 ms"

__icontains

"Gather  (cost=1000.00..8355.17 rows=20 width=721) (actual time=0.997..114.886 rows=927 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on users_user  (cost=0.00..7353.17 rows=8 width=721) (actual time=0.424..90.055 rows=309 loops=3)"
"        Filter: (upper((first_name)::text) ~~ '%ОЛЕГ%'::text)"
"        Rows Removed by Filter: 67167"
"Planning time: 1.993 ms"
"Execution time: 127.784 ms"

__exact

"Gather  (cost=1000.00..8241.51 rows=992 width=721) (actual time=0.800..65.978 rows=916 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on users_user  (cost=0.00..7142.31 rows=413 width=721) (actual time=0.287..38.628 rows=305 loops=3)"
"        Filter: ((first_name)::text = 'Олег'::text)"
"        Rows Removed by Filter: 67170"
"Planning time: 0.191 ms"
"Execution time: 85.363 ms"

__contains

"Gather  (cost=1000.00..8241.81 rows=995 width=721) (actual time=0.732..73.573 rows=916 loops=1)"
"  Workers Planned: 2"
"  Workers Launched: 2"
"  ->  Parallel Seq Scan on users_user  (cost=0.00..7142.31 rows=415 width=721) (actual time=0.134..42.273 rows=305 loops=3)"
"        Filter: ((first_name)::text ~~ '%Олег%'::text)"
"        Rows Removed by Filter: 67170"
"Planning time: 0.195 ms"
"Execution time: 86.438 ms"

С индексами

__iexact

"Index Scan using user_lfm_up_idx on users_user  (cost=0.42..4648.34 rows=1012 width=721) 
(actual time=0.042..32.463 rows=926 loops=1)"
"  Index Cond: (upper((first_name)::text) = 'ОЛЕГ'::text)"
"Planning time: 2.693 ms"
"Execution time: 44.701 ms"

__icontains

"Index Scan using user_lfm_gist_up_idx on users_user  (cost=0.28..23.73 rows=20 width=721)
 (actual time=0.200..39.449 rows=927 loops=1)"
"  Index Cond: (upper((first_name)::text) ~~ '%ОЛЕГ%'::text)"
"Planning time: 0.119 ms"
"Execution time: 51.209 ms"

__exact

"Index Scan using user_lfm_idx on users_user  (cost=0.42..4629.41 rows=992 width=721) 
(actual time=0.037..31.041 rows=916 loops=1)"
"  Index Cond: ((first_name)::text = 'Олег'::text)"
"Planning time: 0.349 ms"
"Execution time: 40.134 ms"

__contains

"Bitmap Heap Scan on users_user  (cost=29.99..1018.67 rows=995 width=721) (actual time=17.723..33.270 rows=916 loops=1)"
"  Recheck Cond: ((first_name)::text ~~ '%Олег%'::text)"
"  Rows Removed by Index Recheck: 11"
"  Heap Blocks: exact=863"
"  ->  Bitmap Index Scan on user_lfm_gist_idx  (cost=0.00..29.74 rows=995 width=0) (actual time=17.614..17.621 rows=927 loops=1)"
"        Index Cond: ((first_name)::text ~~ '%Олег%'::text)"
"Planning time: 0.309 ms"
"Execution time: 44.702 ms"

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

P.S.: Вы можете посмотреть план запроса напрямую из Django, если не хочется лезть в pgadmin:

qs = qs.all()
_iexact = qs.filter(first_name__iexact='олег')
_iexact_explain = qs.explain(analyze=True)

Автоматизация

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

Знакомимся: UpperGistIndex, UpperGistIndexCastedToText, UpperIndex, GistIndex, models.Index

Допустим мы пишем какую-то модель, где у нас будут осуществляться виды поисков: __iexact, __icontains, __exact, __contains, __in. В эту модель нужно добавить индексы, которые можно найти в indexes.py. Подключаются они очень просто, посмотрите пример.

Есть также стандартные индексы, которые можно использовать:

Для операций __exact и __in используется стандартный индекс models.Index()

Для операций __contains используется стандартный индекс GistIndex (from django.contrib.postgres.indexes import GistIndex)

class User(models.Model):
    first_name = models.CharField(
        max_length=64,
        verbose_name='Имя',

    )
    last_name = models.CharField(
        max_length=64,
        verbose_name='Фамилия',
    )

    middle_name = models.CharField(
        max_length=64,
        blank=True,
        verbose_name='Отчество',
    )
    age = models.PositiveSmallIntegerField(
        blank=True,
        verbose_name='Возраст'
    )

    class Meta:
        verbose_name = 'user'
        verbose_name_plural = 'users'
        indexes = [
            UpperGistIndex(fields=['last_name', 'first_name', 'middle_name', ], name='user_lfm_gist_up_idx',
                           opclasses=['gist_trgm_ops', 'gist_trgm_ops', 'gist_trgm_ops', ]),
            GistIndex(fields=['last_name', 'first_name', 'middle_name', ], name='user_lfm_gist_idx',
                      opclasses=['gist_trgm_ops', 'gist_trgm_ops', 'gist_trgm_ops', ]),
            UpperIndex(fields=['last_name', 'first_name', 'middle_name', ], name='user_lfm_up_idx',
                       opclasses=['varchar_pattern_ops', 'varchar_pattern_ops', 'varchar_pattern_ops',]),
            models.Index(fields=['last_name', 'first_name', 'middle_name', ], name='user_lfm_idx'),
            UpperGistIndexCastedToText(fields=['age'], opclasses=['gist_trgm_ops', ], name='user_age_gist_up_idx')
        ]

Пояснения по всем Не стандартным индексам:

Вы пишите в Django ORM (1) = Нужно добавлять индекс (2):

__icontains = UpperGistIndex / UpperGistIndexCastedToText (для числовых полей, "age" в этом примере)

__iexact = UpperIndex

__exact / __in = models.Index

__contains = GistIndex

Для GistIndex, UpperGistIndex, UpperGistIndexCastedToText требуется обязательно указывать opclasses = ['gist_trgm_ops',]

Так же важное уточнение по opclasses их должно быть столько, сколько fields

len(fields) == len(opclasses)

class Meta.indexes доступны с версии Django 1.11, проблем с подключением этих индексов ни у кого не составит труда.

PostgreSQL, миграции

На вашей БД PostgreSQL должно быть установлено расширение pg_trgm.

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Так же нужно добавлять в каждую миграцию, где у вас было создание GistIndex(UpperGistIndex, UpperGistIndexCastedToText) Обязательное создание TrigramExtension 1 строкой в миграции.

from django.contrib.postgres.operations import TrigramExtension

class Migration(migrations.Migration):

    dependencies = [
        ('users', '0001__initial'),
    ]

    operations = [
        TrigramExtension(),
        migrations.AddField()...
    ]

Дополнительно

Для улучшения производительности приложения, убирайте Meta.ordering - очень затратная операция подробнее https://docs.djangoproject.com/en/3.1/ref/models/options/#django.db.models.Options.ordering

Делайте сортировки непосредственно в самом QS.

About

No description or website provided.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy