В этой колонке я буду время от времени делиться некоторыми передовыми статьями о Django. Содержание сосредоточено на сводке навыков и опыта. Источники временно:
Если интересно, прошу обратить внимание и мотивировать.Ведь на перевод и организацию нужно время.Спасибо.
--
Оригинальный адрес: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 с базой данных
Что вы думаете о статье? Я лично считаю, что это здорово!
Если вы считаете, что такая статья очень полезна для вас, обязательно обратите внимание, спасибо