9 вещей, которые вам нужно знать о взаимодействии Django с базой данных

база данных Python Django
9 вещей, которые вам нужно знать о взаимодействии Django с базой данных

В этой колонке я буду время от времени делиться некоторыми передовыми статьями о Django. Содержание сосредоточено на сводке навыков и опыта. Источники временно:

  1. Medium
  2. Twitterизвестный блогер

Если интересно, прошу обратить внимание и мотивировать.Ведь на перевод и организацию нужно время.Спасибо.

--

Оригинальный адрес:9 Django Tips for Working with Databases
Оригинальный автор:Haki Benita
Переводчик:Про книга
Вычитка:Про книга
Уровень рекомендации: ✨✨✨✨✨

Для разработчиков ORM действительно очень полезны, но абстрагирование доступа к базе данных имеет свои издержки, и разработчики, которые хотят исследовать базу данных, часто обнаруживают, что изменение поведения ORM по умолчанию может повысить производительность.

В этой статье я поделюсь 9 советами по работе с базами данных в Django.


1. Агрегация с фильтром

До Django 2.0, если мы хотели получить такие данные, как общее количество пользователей и общее количество активных пользователей, нам приходилось прибегать кусловное выражение:

from django.contrib.auth.models import User
from django.db.models import (
    Count,
    Sum,
    Case,
    When,
    Value,
    IntegerField,
)

User.objects.aggregate(
    total_users=Count('id'),
    total_active_users=Sum(Case(
        When(is_active=True, then=Value(1)),
        default=Value(0),
        output_field=IntegerField(),
    )),
)

В Django 2.0 добавленоАргументы фильтра для агрегатных функций, чтобы было проще:

from django.contrib.auth.models import User
from django.db.models import Count, F

User.objects.aggregate(
    total_users=Count('id'),
    total_active_users=Count('id', filter=F('is_active')),
)

Отлично, коротко и вкусно

Если вы используете PostgreSQL, эти два запроса будут выглядеть так:

SELECT
    COUNT(id) AS total_users,
    SUM(CASE WHEN is_active THEN 1 ELSE 0 END) AS total_active_users
FROM
    auth_users;
SELECT
    COUNT(id) AS total_users,
    COUNT(id) FILTER (WHERE is_active) AS total_active_users
FROM
    auth_users;

Второй запрос используетWHEREоговорка о фильтре.


2. Результаты QuerySet в виде именованных кортежей

Я поклонник namedtuples, а также ORM Django 2.0.

В Джанго 2.0,values_listПараметр метода добавляет параметр с именемnamedхарактеристики. будетnamedУстановить какTrueвернет QuerySet в виде списка именованных кортежей:

> user.objects.values_list(
    'first_name',
    'last_name',
)[0]
(‘Haki’, ‘Benita’)
> user_names = User.objects.values_list(
    'first_name',
    'last_name',
    named=True,
)
> user_names[0]
Row(first_name='Haki', last_name='Benita')
> user_names[0].first_name
'Haki'
> user_names[0].last_name
'Benita'

3. Пользовательские функции

ORM Django 2.0 очень мощный и многофункциональный, но он все еще не синхронизирован со всеми функциями базы данных. К счастью, ORM позволяет нам расширять его с помощью пользовательских функций.

Предположим, у нас есть поле продолжительности, в которое записываются отчеты, и мы хотим найти среднюю продолжительность всех отчетов:

from django.db.models import Avg
Report.objects.aggregate(avg_duration=Avg(‘duration’))
> {'avg_duration': datetime.timedelta(0, 0, 55432)}

Это здорово, но немного менее информативно, если есть только среднее значение. Рассчитаем стандартное отклонение:

from django.db.models import Avg, StdDev
Report.objects.aggregate(
    avg_duration=Avg('duration'),
    std_duration=StdDev('duration'),
)
ProgrammingError: function stddev_pop(interval) does not exist
LINE 1: SELECT STDDEV_POP("report"."duration") AS "std_dura...
               ^
HINT:  No function matches the given name and argument types.
You might need to add explicit type casts.

Э... PostgreSQL не поддерживает стандартное отклонение для полей типа интервала, нам нужно преобразовать интервал в число, прежде чем мы сможем применить его к нему.STDDEV_POPработать.

Один из вариантов — извлечь из интервала:

SELECT
    AVG(duration),
    STDDEV_POP(EXTRACT(EPOCH FROM duration))
FROM 
    report;

      avg       |    stddev_pop    
----------------+------------------
 00:00:00.55432 | 1.06310113695549
(1 row)

Итак, как мы реализуем это в Django? Вы уже догадались - пользовательская функция:

# common/db.py
from django.db.models import Func

class Epoch(Func):
   function = 'EXTRACT'
   template = "%(function)s('epoch' from %(expressions)s)"

Наша новая функция используется следующим образом:

from django.db.models import Avg, StdDev, F
from common.db import Epoch

Report.objects.aggregate(
    avg_duration=Avg('duration'), 
    std_duration=StdDev(Epoch(F('duration'))),
)
{'avg_duration': datetime.timedelta(0, 0, 55432),
 'std_duration': 1.06310113695549}

*Обратите внимание на использование F-выражений в вызовах Epoch.


4. Время ожидания заявления

Это, наверное, самый простой и самый важный совет, который я могу дать. Мы люди, и все мы ошибаемся. Мы не можем учитывать каждый пограничный случай, поэтому мы должны установить границы.

В отличие от других неблокирующих серверов приложений, таких как Tornado, asyncio и даже Node, Django обычно использует синхронные рабочие процессы. Это означает, что когда пользователь выполняет длительную операцию, рабочий процесс блокируется, и никто другой не может его использовать, пока он не завершится.

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

В большинстве приложений Django большую часть времени тратится на ожидание запросов к базе данных. Итак, установка тайм-аутов для SQL-запросов — хорошее место для начала.

мне нравится это в моемwsgi.pyУстановите глобальный тайм-аут в файле:

# wsgi.py
from django.db.backends.signals import connection_created
from django.dispatch import receiver

@receiver(connection_created)
def setup_postgres(connection, **kwargs):
    if connection.vendor != 'postgresql':
        return
    
    # Timeout statements after 30 seconds.
    with connection.cursor() as cursor:
        cursor.execute("""
            SET statement_timeout TO 30000;
        """)

Почему wsgi.py?Потому что тогда это повлияет только на рабочие процессы, а не на внепроцессные аналитические запросы, задачи cron и т.д.

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

Тайм-ауты также можно настроить для детализации пользователя:

postgresql=#> alter user app_user set statement_timeout TO 30000;
ALTER ROLE

Не по теме: мы проводим много времени в других общих местах, таких как Интернет. Поэтому всегда устанавливайте тайм-аут при вызове удаленных служб:

import requests

response = requests.get(
    'https://api.slow-as-hell.com',
    timeout=3000,
)

5. Ограничение

Это в некоторой степени связано с последним пунктом об установлении границ. Иногда поведение некоторых наших клиентов непредсказуемо

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

Вот почему ограничивайте

Мы ограничиваем запрос, чтобы он возвращал не более 100 строк данных:

# bad example
data = list(Sale.objects.all())[:100]

Это плохо, потому что даже если возвращается только 100 строк данных, вы фактически извлекаете все строки в память.

Давай попробуем еще:

data = Sale.objects.all()[:100]

Это намного лучше, Django будет использовать предложение limit в SQL, чтобы получить 100 строк данных.

Мы увеличили лимит, но у нас осталась проблема — пользователь хочет все данные, а мы ему дали только 100, а пользователь теперь думает, что данных только 100.

Вместо того, чтобы слепо возвращать первые 100 строк, давайте убедимся, что если строк больше 100 (обычно после фильтрации), мы выдадим исключение:

LIMIT = 100

if Sales.objects.count() > LIMIT:
    raise ExceededLimit(LIMIT)
return Sale.objects.all()[:LIMIT]

Работает хорошо, но мы добавили новый запрос

Можно ли сделать лучше? Мы можем сделать это:

LIMIT = 100

data = Sale.objects.all()[:(LIMIT + 1)]
if len(data) > LIMIT:
    raise ExceededLimit(LIMIT)
return data

Мы не берем 100 строк, мы берем 100 + 1 = 101 строку, если существует 101 строка, то мы знаем, что существует более 100 строк:

Запомните трюк LIMIT + 1, иногда это очень удобно


6. Управление транзакциями и блокировками

Это сложнее.

Мы начали видеть ошибки тайм-аута транзакции посреди ночи из-за механизма блокировки в базе данных.

(Кажется, этого автора часто будили среди ночи перед этим 🤣)

Общий шаблон для манипулирования транзакциями в нашем коде выглядит так:

from django.db import transaction as db_transaction

...
with db_transaction.atomic():
  transaction = (
        Transaction.objects
        .select_related(
            'user',
            'product',
            'product__category',
        )
        .select_for_update()
        .get(uid=uid)
  )
  ...

Транзакционные операции обычно включают некоторые атрибуты пользователей и продуктов, поэтому мы часто используемselect_relatedчтобы принудительно присоединиться и сохранить некоторые запросы.

Обновление транзакции также включает в себя получение блокировки, чтобы гарантировать, что она не может быть получена другими.

Теперь вы видите проблему? нет? у меня тоже нет. (Автор такой милый)

У нас есть несколько процессов ETL, которые запускаются ночью, в основном выполняя обслуживание таблиц продуктов и пользователей. Эти операции ETL обновляют поля, а затем вставляют их в таблицу, поэтому они также получают блокировку таблицы.

Так в чем проблема? когдаselect_for_updateиselect_relatedПри совместном использовании Django попытается заблокировать все таблицы в запросе.

Код, который мы используем для получения транзакций, пытается получить блокировки для таблицы транзакций, пользователя, продукта, таблицы категорий. Как только ETL блокирует последние три таблицы в полночь, транзакции начинают терпеть неудачу.

Как только мы лучше поняли проблему, мы начали искать способы блокировки только нужных таблиц (транзакционных таблиц). (опять же) к счастью,select_for_updateВ Django 2.0 доступна новая опция:

from django.db import transaction as db_transaction

...
with db_transaction.atomic():
  transaction = (
        Transaction.objects
        .select_related(
            'user',
            'product',
            'product__category',
        )
        .select_for_update(
            of=('self',)
        )
        .get(uid=uid)
  )
  ...

этоofопция была добавлена ​​вselect_for_update,использоватьofМы можем указать таблицу, которую хотим заблокировать,selfэто специальное ключевое слово, означающее, что мы хотим заблокировать модель, над которой мы работаем, таблицу транзакций.

В настоящее время эта функция доступна только для PostgreSQL и Oracle.


7. Индексы внешнего ключа (индексы FK)

При создании модели Django создает индекс B-Tree для всех внешних ключей, что может быть довольно затратным, а иногда и ненужным.

Типичным примером является сквозная модель отношения M2M (многие ко многим):

class Membership(Model):
    group = ForeignKey(Group)
    user = ForeignKey(User)

В приведенной выше модели Django неявно создаст два индекса: один для пользователей и один для групп.

Еще один распространенный шаблон в моделях M2M — наличие двух полей вместе в качестве уникального ограничения. В данном случае это означает, что пользователь может быть членом только той же группы или этой модели:

class Membership(Model):
    group = ForeignKey(Group)
    user = ForeignKey(User)
    class Meta:
        unique_together = (
           'group',
           'user',
        )

этоunique_togetherтакже создает два индекса, поэтому мы получаемдваполятрипроиндексированные модели 😓

В зависимости от функций, которые мы используем для этой модели, мы можем игнорировать индекс FK и оставить только уникальный индекс ограничения:

class Membership(Model):
    group = ForeignKey(Group, db_index=False)
    user = ForeignKey(User, db_index=False)
    class Meta:
        unique_together = (
            'group',           
            'user',
        )

Удаление избыточных индексов сделает вставки и запросы быстрее, а нашу базу данных - легче.


8. Порядок столбцов в составном индексе

Индекс с несколькими столбцами называется составным индексом. В составном индексе B-Tree первый столбец индексируется с использованием древовидной структуры. Создайте новое дерево для второго слоя из листьев первого слоя и так далее.

Порядок столбцов в индексе очень важен.

В приведенном выше примере мы сначала получим дерево групп и еще одно дерево со всеми его пользователями.

Эмпирическое правило для составных индексов B-Tree заключается в том, чтобы вторичные индексы были как можно меньше. Другими словами, столбцы с высокой кардинальностью (более явные значения) должны быть первыми.

В нашем случае предполагается, что групп меньше, чем пользователей (в целом), поэтому размещение столбца пользователей первым приведет к уменьшению вторичного индекса для групп.

class Membership(Model):
    group = ForeignKey(Group, db_index=False)
    user = ForeignKey(User, db_index=False)
    class Meta:
        unique_together = (
            'user',
            'group',
        )

*Обратите внимание на порядок названий полей в кортеже.

Это просто эмпирическое правило, окончательный индекс должен быть оптимизирован для конкретного сценария. Суть здесь в том, чтобы знать важность порядка столбцов в неявных и составных индексах. (Линь Шу: Нельзя обижаться, нельзя обижаться)


9. BRIN-индексы

Индекс B-Tree структурирован как дерево. Стоимость поиска одного значения равна высоте дерева таблицы произвольного доступа + 1. Это делает индексы B-Tree идеальными для уникальных ограничений и (некоторых) запросов диапазона.

Недостатком индекса B-Tree является его размер — индекс B-Tree может стать больше.

Нет других вариантов? Нет, существует довольно много баз данных, которые предоставляют другие типы индексов для конкретных случаев использования.

Начиная с Django 1.11, появилась новая опция Meta для создания индексов для моделей. Это дает нам возможность исследовать другие типы индексов.

PostgreSQL имеет очень полезный тип индекса BRIN (индекс диапазона блоков). В некоторых случаях индексы BRIN могут быть более эффективными, чем индексы B-Tree.

Давайте посмотрим, что говорит официальная документация сайта:

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

Чтобы понять это утверждение, важно понять, как работают индексы BRIN. Как следует из названия, индекс BRIN создает небольшой индекс для ряда смежных блоков в таблице. Индекс очень мал и может только сказать, находится ли значение в диапазоне или находится ли оно в диапазоне блока индекса.

Давайте рассмотрим простой пример того, как индекс BRIN может нам помочь.

Предположим, у нас есть эти значения в столбце, каждое в блоке:

1, 2, 3, 4, 5, 6, 7, 8, 9

Мы создаем диапазон для каждых трех соседних блоков:

[1,2,3], [4,5,6], [7,8,9]

Для каждого диапазона мы сохраним минимальное и максимальное значения в диапазоне:

[1–3], [4–6], [7–9]

Пробуем искать 5 по этому индексу:

  • [1–3] —  точно не здесь
  • [4–6] —  может быть здесь
  • [7–9] —  точно не здесь

Используя индекс, мы ограничиваем наш поиск диапазоном [4-6].

Другой пример, на этот раз значения в столбце будут плохо отсортированы:

[2–9], [1–7], [3–8]

Попробуйте снова найти 5:

  • [2–9] —  может быть здесь
  • [1–7] —  может быть здесь
  • [3–8] —  может быть здесь

Индекс бесполезен — он не только не ограничивает поиск, нам фактически приходится искать больше, потому что мы извлекаем индекс и всю таблицу одновременно.

Вернемся к документации:

... столбцы имеют некоторую естественную корреляцию с физическим расположением в таблице

Это ключ к индексу BRIN. Чтобы в полной мере воспользоваться им, значения в столбце должны быть грубо отсортированы или сгруппированы на диске.

Теперь вернемся к Django. Каковы наши часто индексируемые поля, которые, скорее всего, будут естественным образом отсортированы на диске? Верноauto_now_add. (Это очень часто используется, и те, кто его не использует, могут его понять)

Очень распространенный шаблон в моделях Django:

class SomeModel(Model):    
    created = DatetimeField(
        auto_now_add=True,
    )

когда используешьauto_now_add, Django автоматически заполнит строку текущим временем. Созданное поле также обычно является хорошим кандидатом для запроса, поэтому оно обычно вставляется в индекс.

Давайте добавим индекс BRIN во время создания:

from django.contrib.postgres.indexes import BrinIndex
class SomeModel(Model):
    created = DatetimeField(
        auto_now_add=True,
    )
    class Meta:
        indexes = (
            BrinIndex(fields=['created']),
        )

Чтобы понять разницу в размере, я создал таблицу примерно из 2 миллионов строк с естественно отсортированным полем даты на диске:

  • Индекс B-дерева: 37MB
  • BRIN-индекс: 49KB

Да, вы правильно прочитали.

При создании индекса следует учитывать гораздо больше, чем размер индекса. Но теперь, когда Django 1.11 поддерживает индексы, мы можем легко включать новые типы индексов в наши приложения, делая их легче и быстрее.

--

После того, как полный текст будет готов, вы можете перепечатать его по желанию, с указанием источника для перепечатки:9 вещей, которые вам нужно знать о взаимодействии Django с базой данных

Что вы думаете о статье? Я лично считаю, что это здорово!

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