Насколько быстрее ваше приложение с индексами?

Даже «старшие» разработчики часто забывают использовать индексы в Django. Вы даже представить себе не можете, насколько он может стать быстрее, если вы просто добавите в свою модель две строки кода!

В этой статье мы будем снимать мерки вместе: с индексами и без них.

Настраивать

Нашим пациентом будет небольшое приложение Django всего с 7 полями.

Я просто покажу модель здесь:

class Student(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    code0 = models.CharField(max_length=100, blank=True)
    code1 = models.CharField(max_length=100, blank=True)
    code2 = models.CharField(max_length=100, blank=True)
    code3 = models.CharField(max_length=100, blank=True)
    code4 = models.CharField(max_length=100, blank=True)

У нас много CharFields. Индексов нет.

Мы будем тестировать 100K строк следующим образом:

from django.test import TestCase
from student.models import Student
import datetime


class StudentTestCase(TestCase):
    def setUp(self):
        start_time = datetime.datetime.now()
        students = []
        batch_size = 500
        for i in range(100000):
            student = Student()
            student.first_name = str(i)
            student.last_name = str(i)
            student.code0 = f"code{i}"
            students.append(student)
        Student.objects.bulk_create(students, batch_size)

        end_time = datetime.datetime.now()
        print(f" Created in {end_time - start_time}")

    def test_lookup(self):
        start_time = datetime.datetime.now()
        for i in range(50000, 51000):
            Student.objects.get(code0=f"code{i}")

        end_time = datetime.datetime.now()
        print(f"Looked up in {end_time - start_time}")

Сначала мы создаем 100К элементов. А затем прямо в середине базы данных мы будем искать 1000 из них.

Теперь мы переносим и запускаем тест.

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Created in 0:00:03.429665
Looked up in 0:00:05.001903
----------------------------
Ran 1 test in 8.504s
OK

Поиск 1000 элементов занял 5,1 секунды.

Наш первый индекс

Теперь мы представим простой индекс с одним полем. Конечно, это полеcode0:

class Student(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    code0 = models.CharField(max_length=100, blank=True)
    code1 = models.CharField(max_length=100, blank=True)
    code2 = models.CharField(max_length=100, blank=True)
    code3 = models.CharField(max_length=100, blank=True)
    code4 = models.CharField(max_length=100, blank=True)

    class Meta:
        indexes = [models.Index(fields=['code0', ]), ]

Вы можете видеть, что для нашей модели добавлены некоторые метаданные.

Сделайте миграции, перенесите, запустите тест:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Created in 0:00:03.473439
Looked up in 0:00:00.3344573 <---
-------------------
Ran 1 test in 3.840s
OK

Время поиска в базе данных для 1000 элементов составляет 0,3 секунды против 5,1!

Это ровно в 15 раз быстрее! Как и обещал. Если честно, я сначала измерил, а потом придумал название для этой статьи.

Комплексные индексы

Часто нужно искать 2 поля.

Итак, немного меняем наш тест.

class StudentTestCase(TestCase):
    def setUp(self):
        start_time = datetime.datetime.now()
        students = []
        batch_size = 500
        for i in range(100000):
            student = Student()
            student.first_name = str(i)
            student.last_name = str(i)
            student.code0 = f"code0{i%2}"
            student.code1 = f"code1{i}"
            students.append(student)
        Student.objects.bulk_create(students, batch_size)

        end_time = datetime.datetime.now()
        print(f" Created in {end_time - start_time}")

    def test_lookup(self):
        start_time = datetime.datetime.now()
        for i in range(50000, 51000):
            Student.objects.filter(code0=f"code0{i%2}").get(code1=f"code1{i}")

        end_time = datetime.datetime.now()
        print(f"Looked up in {end_time - start_time}")

А именно, сейчас мы запрашиваем два поля - code0 и code1.

Для базы данных это намного сложнее. И потребовалось

Looked up in 0:00:12.380889

12,3 секунды на его обработку. Хотя у нас есть индекс для code0.

Должны ли мы добавить индекс? Давайте сначала попробуем не ту версию:

class Student(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    code0 = models.CharField(max_length=100, blank=True)
    code1 = models.CharField(max_length=100, blank=True)
    code2 = models.CharField(max_length=100, blank=True)
    code3 = models.CharField(max_length=100, blank=True)
    code4 = models.CharField(max_length=100, blank=True)

    class Meta:
        indexes = [models.Index(fields=['code0', ]),
                   models.Index(fields=['code1', ])]

Сделайте миграции, перенесите и запустите тест:

Looked up in 0:00:00.450037

Уже неплохо. Это в 27 раз быстрее!

Но правильный способ создания индекса в этом случае следующий:

class Meta:
    indexes = [models.Index(fields=['code0', 'code1']),]

Для нашего простого теста это не намного быстрее, это 0,42 секунды. против 0,45 секунды Но для огромных и более сложных баз данных это действительно имеет значение.

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

Интересный факт: если у вас проиндексировано только одно поле, скажем, code0, фильтрация сначала по code0, а затем по code1 выполняется так же быстро, как фильтрация по code1, а затем по code0

Вот пример:

Student.objects.filter(code0=f"code0{i%2}").get(code1=f"code1{i}")

так же быстро, как

Student.objects.filter(code0=f"code1{i}").get(code1=f"code0{i%2}")

даже если проиндексировано только одно из полей.

Почему с индексом быстрее?

Когда вы делаете запрос, система управления базами данных (СУБД) должна пройти через всю вашу полную базу данных со всеми вашими полями в том же порядке, в котором они были сохранены. Если в вашей базе данных 1 миллион записей, она должна пройти 1 миллион строк и проверить ваш запрос.

Как этого избежать? Мы должны отсортировать и разделить интересующие нас столбцы.

Итак, когда вы создаете индекс, СУБД создает другую таблицу, содержащую только необходимые поля. Так что эта таблица намного меньше и уже удобнее для поиска. Но не так уж и много.

СУБД сортирует строки в этой специальной таблице!

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

Вот что такое индекс.

Почему в Django по умолчанию нет индексов?

Если индексы настолько хороши, почему по умолчанию не создаются индексы для всех столбцов? В этом есть смысл, не так ли?

Потому что каждый раз, когда вы меняете свою базу данных, все индексы должны обновляться. Итак, вам нужно создать 100К записей? Если у вас всего один индекс, он должен создать за вас 200 000 записей. Если у вас два, то 300К записей. Потому что теперь у нас больше таблиц, и все они должны быть на одной странице.

Более того, он занимает ваше место на жестком диске.

Более того, СУБД не может знать, какие комбинации вы собираетесь использовать в своем запросе. Вы когда-нибудь будете запрашивать имя вместе с фамилией? Иногда даже программист не знает этого заранее.

Хорошо, но тогда вставка в базу данных становится медленнее?

Да, это может быть медленнее, но обычно это не проблема. Обычно записи не вставляются группами, они вставляются по одной. Или до 10 записей за раз (да, в системе ERP одно действие может привести к вставке в 10 таблиц по 100 строк в каждой, а иногда и больше). Таким образом, при создании нового пользователя это может быть на десятые доли секунды медленнее. Какая разница?

Смотрите мою другую статью по оптимизации Django: