Насколько быстрее ваше приложение с индексами?
Даже «старшие» разработчики часто забывают использовать индексы в 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: