Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Олег Чуркин (Rambler&Co) - Django: правильно го...

Олег Чуркин (Rambler&Co) - Django: правильно готовим ORM

Доклад с конференции Moscow Python Conf 2016 (http://conf.python.ru)
Видео: https://conf.python.ru/django-orm/

В докладе будут затронуты большинство тем, которые необходимо знать современному python-разработчику, чтобы эффективно использовать функционал Django-ORM для построения высоконагруженных web-проектов.
Поговорим и про классические ошибки при работе с QuerySet’ами и про профилирование и про code style. Выясним как можно сэкономить память и время при выполнении запросов, покажу популярные ошибки при проектировании схемы данных и при использовании миграций, а так же рассмотрим несколько распространенных задач современного веба, которые в Django еще не решены или решены некорректно.

Avatar for Moscow Python Meetup

Moscow Python Meetup

October 12, 2016
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. Как мы работаем с ORM? • Создаем или изменяем схему

    данных (описание моделей и миграции) • Манипулируем данными (QuerySet → SQL) Но сначала поговорим про наше любимое – про стиль.
  2. Django Coding Style https://docs.djangoproject.com/en/1.10/internals/contributing/writing-code/coding-style/ • Называем модели в единственном числе:

    не Users, а User, Books → Book, т.к. экземпляр модели отражает строку в таблице • Следует придерживаться определенного порядка атрибутов в модели: поля, менеджеры, Meta, __unicode__, save и т.д.
  3. Схема БД: теория • Нормальные формы и их применение для

    нормализации данных. • Типы данных → типы полей • Ограничения (constraints) и индексы (indexes), их типы и области применения. • Небезопасные изменения схемы (миграции)
  4. class Category(models.Model): title = models.CharField() parent = models.ForeignKey('self', null=True) class

    Article(models.Model): title = models.CharField() category = models.ForeignKey(Category) parent_category = models.ForeignKey(Category) author_name = models.CharField() author_url = models.CharField() Django: соблюдаем нормальные формы
  5. class Category(models.Model): title = models.CharField() parent = models.ForeignKey('self', null=True) class

    Article(models.Model): title = models.CharField() category = models.ForeignKey(Category) parent_category = models.ForeignKey(Category) author_name = models.CharField() author_url = models.CharField() Нарушена 3-я нормальная форма Зависимость article → parent_category является транзитивной: article → category → parent
  6. class Category(models.Model): title = models.CharField() parent = models.ForeignKey('self') class Article(models.Model):

    title = models.CharField() category = models.ForeignKey(Category) parent_category = models.ForeignKey(Category) author_name = models.CharField() author_url = models.CharField() Нарушена форма Бойса-Кодда Оба поля author_* являются составным внешним ключом → стоит вынести в отдельную модель
  7. class Category(models.Model): title = models.CharField() parent = models.ForeignKey('self', null=True) class

    Author(models.Model): name = models.CharField() url = models.URLField() class Article(models.Model): title = models.CharField() category = models.ForeignKey(Category) author = models.ForeignKey(Author) Нормализуем модели
  8. • Некоторые оптимизации в БД рассчитаны именно на нормализованные данные

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

    обоснование и учитывать все риски • Старайтесь избегать хранения ссылок на другие таблицы в полях-структурах (Array, JSON, CommaSeparated) • Обычно денормализация требуется на более поздних этапах при высоких нагрузках Денормализация – это плохо?
  10. Рекомендации по типам полей • Для хранения "денежных" значений только

    DecimalField • Чем точнее выбран тип данных, тем меньше места займет он в таблице • В PostgreSQL одинаковая реализация CharField и TextField
  11. Очевидные ограничения (constraints) • Лучшие кандидаты на "уникальность" (unique) –

    это идентификаторы чего-либо, например электронная почта, телефон или поле типа SlugField • Обязателен unique_together на внешние ключи, если вы используете свою промежуточную модель для many-to-many связей
  12. Очевидные индексы (indexes) • Лучшие кандидаты для индексирования (index_db) –

    это поля, по которым будет осуществляться выборка данных, сортировка или поиск (LIKE "foo%") • index_together – если нужно ускорить выборку сразу по нескольким полям, например (pub_date, project_id)
  13. Миграции в Django • Атомарные по умолчанию (в Django 1.10

    можно отключить – atomic=False), все изменения происходят в одной транзакции • Не импортируйте модели в файле с миграцией – используйте apps.get_model • Автогенерация миграций не всегда работает оптимально для ваших таблиц и данных
  14. • Некоторые запросы ALTER TABLE берут ACCESS EXCLUSIVE LOCK, которая

    блокирует таблицу на чтение и запись • Изменение схемы может быть заблокировано запросом в статусе IDLE IN TRANSACTION (ABORTED) • CREATE (DROP) INDEX блокирует таблицу на запись • VACUUM FULL полностью блокирует таблицу Что значит не всегда оптимально?
  15. Небезопасные миграции Проблема Решение CREATE (DROP) INDEX CREATE (DROP) INDEX

    CONCURRENTLY Изменение типа данных в колонке Добавляем новую колонку, меняем код, чтобы он использовал обе, удаляем старую колонку Добавление колонки со значением по умолчанию Отдельно добавляем колонку и отдельно проставляем ей значение по умолчанию и заполняем этим значением
  16. Небезопасные миграции #2 Проблема Решение Добавление колонки с UNIQUE- ограничением

    Добавляем новую колонку, создаем уникальный индекс конкурентно, создаем ограничение на основе индекса Добавление новой NOT NULL колонки, изменение длины VARCHAR Все очень сложно VACUUM FULL Лучше посмотреть в сторону pg_repack
  17. На что стоит обратить внимание • db_index=True вызывает неконкурентный CREATE

    INDEX, используйте atomic=False чтобы вызвать в миграции CREATE INDEX CONCURRENTLY • Чем меньше операций выполняется в одной транзакции (миграции), тем безопасней. • После создания или удаления индекса рекомендуется выполнить ANALYZE TABLE
  18. Как лучше провести миграции • Отдельно от кода проекта (иногда

    в три этапа) • Проверяем что нет запросов в статусе IDLE IN TRANSACTION • pg_activity • Любой другой мониторинг вашей БД (New Relic, okmeter и т. д.)
  19. Если вкратце, то... • Соблюдение стиля – красиво • Нормализация

    – полезно • Индексы и ограничения – быстро и консистентно • Миграции – не так просто как кажется
  20. Взаимодействие с данными • Экономьте запросы: минимизируйте количество запросов и

    оптимизируйте их по скорости • Экономьте память приложения и сетевой трафик: запрашивайте только те данные, которые необходимы • БД всегда быстрее чем Python: агрегацию, сортировку и удаление дубликатов выгоднее выполнять на уровне СУБД.
  21. Анализируем количество и качество SQL- запросов • Django: логирование в

    Django через логгер django.db.backends • Django: django-debug-toolbar или\и django-devserver • PostgreSQL: log_min_duration_statement • PostgreSQL: pg_stat_statements • PostgreSQL: EXPLAIN (ANALYZE, BUFFERS) • PgBadger – простой, но мощный инструмент
  22. Ошибка новичка "Замеряем" время выполнения запроса start_time = time.time() try:

    articles = Article.objects.all() finally: end_time = time.time() measure_request_time(start_time - end_time)
  23. "Ленивые" QuerySet'ы start_time = time.time() try: articles = Article.objects.all() finally:

    end_time = time.time() measure_request_time(start_time - end_time) Запроса еще не происходит. ORM выполняет запрос в БД только при определенном взаимодействии с QuerySet'ом: QuerySet evaluation
  24. Выполняем JOIN-ы явно, с помощью select_related: # N + 1

    запрос к БД for article in Article.objects.all(): print(article.title, article.author.name) # 1 запрос к БД и JOIN for article in Article.objects.select_related('author'): print(article.title, article.author.name) Django: экономим запросы
  25. We need to go deeper Но лучший вариант использовать values:

    # 1 запрос к БД, JOIN + экономим время, память и трафик for article in Article.objects.values('title', 'author__name'): print(article['title'], article['author__name']) А также only, defer и values_list для получения списка значений какого-либо поля.
  26. Экономим запросы: prefetch_related • Подгружаем данные для связей many-to-many и

    many-to-one за один запрос. • Важно: использует SQL вида WHERE ... IN (<огромный список ID-шников>) • Почему это негативно влияет на производительность, рассказывает Илья Космодемьянский (PostgreSQL-Consulting.com): http://highload.guide/blog/query_performance_postgreSQL.html
  27. Используйте exists() вместо count() • count() – агрегационная функция и

    БД приходится выполнить последовательное сканирование всей таблицы (или индекса) • exists() – выполняет запрос с LIMIT 1 • На таблице из ~20,000 элементов разница в времени выполнения достигает 150 раз (0.026 ms vs 4.009 ms)
  28. При итерировании по результатам запроса не создаем экземпляры объектов для

    всех записей: # 10 экземпляров Article в памяти for article in Article.objects.all()[:10]: print(article.title) # 1 экземпляр Article в памяти for article in Article.objects.iterator()[:10]: print(article.title) Экономим память с помощью iterator()
  29. Используем внешний ключ правильно article = Article.object.get(...) # дополнительный запрос

    в БД category_id = article.category.id # правильное решение category_id = article.category_id # запрос на получение объектов articles = Article.objects.filter(category_id=category_id)
  30. В качестве значений полей можно передавать QuerySet'ы и Django-ORM самостоятельно

    оформит их как подзапросы: # Только 1 запрос к БД с подзапросом Article.objects.filter(category_in=Category.objects.filter(...)) Используем подзапросы
  31. А также • Не используйте order_by("?") • Избегайте выполнения запросов

    к БД во время инициализации приложения • Не бойтесь использовать "чистый" SQL но помните об SQL-инъекциях.
  32. Транзакции Главное правило работы с транзакциями – если в коде

    вы выполняете несколько связанных операций UPDATE или DELETE подряд, то их необходимо обернуть в transaction.atomic, иначе есть большая вероятность получить неконсистентные данные в БД. Если хотим протестировать какую-нибудь логику связанную с транзакциями – используйте TransactionTestCase.
  33. Атомарные операции Увеличиваем баланс пользователя и получаем состояние гонки между

    #1 и #2. Используем F() чтобы выполнить операцию в SQL, а не в Python: user = User.objects.get() # 1 # неправильно user.balance += 100 # 2 # правильно user.balance = F('balance') + 100 #2 user.save(update_fields=('balance',))
  34. Классические проблемы в WEB • Выбрать TOP N элементов из

    каждой группы (N самых свежих статей по каждой теме) • Реализовать постраничный вывод данных (пагинация) • Работа со слабоструктурированными данными (интернет магазин)
  35. Классический подход: N + 1 запрос: for category in categories:

    latest_articles_by_category[category] = ( Article.objects.filter(category=category) .order_by('-date_published')[:N] ) Количество запросов растет линейно с количеством категорий – медленно на больших таблицах. Выборка TOP N элементов
  36. Оконные функции (window functions) PostgreSQL 9.1: выполняем операции над записями,

    которые находятся в окне: SELECT * from ( SELECT a.*, row_number() OVER (PARTITION BY a.category_id ORDER BY a.date_published DESC) as row FROM article AS a JOIN category AS c ON a.category_id = c.id AND c.id IN (1, 2, 3, 4) ) as s WHERE s.row <= N;
  37. Но еще быстрее: LATERAL JOIN PostgreSQL 9.3: LATERAL JOIN –

    выполняет второй подзапрос в поле FROM для каждого элемента из первого подзапроса: SELECT sa.* FROM (SELECT * FROM category WHERE id IN (1, 2, 3, 4)) as c JOIN LATERAL ( SELECT * FROM article as a WHERE a.category_id = c.id ORDER BY a.date_published DESC, a.id DESC LIMIT 5 ) as sa ON TRUE;
  38. Классическая пагинация В Django реализована в виде класса Paginator, проблемы:

    • COUNT (Execution time: 7145.938 ms) • OFFSET X – БД нужно прочитать с диска X записей после соответствующей сортировки; неверный список элементов, если новый элемент был вставлен на страницу, которую уже запросили.
  39. Решение: KEYSET пагинация • Запоминаем идентификаторы первого FIRST_TIMESTAMP и последнего

    LAST_TIMESTAMP элемента на странице, например это может быть дата публикации. • Чтобы получить следующую страницу используем конструкцию WHERE table.date > LAST_TIMESTAMP ORDER BY date ASC LIMIT <PAGE SIZE> • Для предыдущей страницы: WHERE table.date < FIRST_TIMESTAMP ORDER BY date DESC LIMIT <PAGE SIZE>
  40. KEYSET пагинация Плюсы: • Работает ощутимо быстрее на большом количестве

    данных. • Правильно обрабатывает изменение предыдущих страниц при добавлении новых элементов. Минусы: • Нельзя перейти на произвольную страницу, без предварительной подготовки данных, поэтому хорошо применима только для «бесконечного скроллинга». • Нет полноценной реализации для Django, django-infinite-scroll-pagination – слишком примитивен и давно не обновляется.
  41. Непростые решения в реляционных СУБД. • А что если нам

    требуется динамически изменять схему данных? Возможность добавить произвольное поле в таблицу из админки. • Хранение слабоструктурированных данных: в нашем интернет магазине тысячи товаров, у которых набор полей может отличаться. • Небольшой «тюнинг» для объектов в БД – слишком много булевых колонок.
  42. Entity-Attribute-Value Плюсы: • Есть хорошая реализация для Django: django-eav •

    Знакомая реляционная семантика Минусы: • Значения атрибутов не типизированы • Сложно задать обязательные атрибуты (NOT NULL) • Любые сложные запросы превращаются в нагромождение JOIN’ов • Производительность на большом количестве данных
  43. Альтернативы? • HSTORE – key/value, не поддерживает вложенность • JSON

    – обычный текст, не поддерживает индексирование • JSONB – бинарный формат, занимает чуть больше места, но поддерживает индексирование
  44. JSONB (PostgreSQL 9.4+) Плюсы: • Проще и понятней. • Быстрее

    чем EAV, особенно с GIN индексом. • Данные + индексы занимают меньше места (раза в 3) • Поддерживает хранение различных типов данных внутри JSON формата, а не только строки.
  45. Минусы: • Поддерживаются не все инструменты и подходы, которые мы

    используем для реляционных данных (не все индексы и ограничения, ограниченный функционал запросов). • Неочевидный синтаксис запросов (->>, ->, @>, ?&, ?|). JSONB (PostgreSQL 9.4+)
  46. JSQUERY extension: новые возможности по поиску данных в jsonb: •

    Поиск элемента в массиве • Поиск массивов с определенным размером • Поиск во вложенных элементах через wildcards • Добавляет возможность использовать CHECK CONSTRAINT И многое другое: https://github.com/postgrespro/jsquery/ JSONB на стероидах
  47. JSONB в Django-ORM В версии 1.9 доступно JSONField, поддеживает основные

    операции: • contains • contained_by • has_key • has_any_keys • has_keys • values Запросы jsquery не поддерживаются.
  48. Выводы • Логгирование и мониторинг – must have для любого

    разработчика\проекта • Идеальный QuerySet генерирует SQL запрос, который бы вы написали руками для решения своей задачи • Встроенные в ORM инструменты используют известные, но не самые быстрые подходы к обработке данных • Писать "чистый" SQL – это нормально