Как принудительно использовать тип данных char (n) вместо nvarchar в Django запросах с django -ms sql -backend - PullRequest
0 голосов
/ 16 апреля 2020

Я использую, python 3.7, django 2.2 и django -ms sql -обэкэнда с устаревшей базой данных Microsoft SQL Server 2012. Я сгенерировал модель, используя inspectdb. Модель имеет поле с именем «ord_no», которое является типом данных CHAR (8) в базе данных. Django правильно создает его как CharField (max_length = 8). Примечание. Для этой таблицы имеется индекс в столбце ord_no. Вот упрощенное определение таблицы:

CREATE TABLE [dbo].[imordbld_sql](
    [ord_no] [char](8) NOT NULL,
    [item_no] [char](30) NOT NULL,
    [qty] [decimal](13, 4) NULL,
    [qty_per] [decimal](15, 6) NULL,
    [ID] [numeric](9, 0) IDENTITY(1,1) NOT NULL
) ON [PRIMARY]
GO

Вот мой models.py с упрощенной версией рассматриваемой модели с полями, автоматически сгенерированными с помощью inspectdb. Я также создал собственный менеджер моделей, который преобразует Параметр ord_no в поле CHAR (8) для демонстрационных целей.

models.py

from django.db import models


class OrderBuildManager(models.Manager):

    """
    Django automatically use nvarchar(16) instead of char(8) when using the ORM to   filter by work order.
    This method casts the ord_no parameter to a char(8) so SQL can use an index on the ord_no
    column more efficiently (index seek vs index scan)
    """

    def get_order(self, ord_no):
        qs = super(OrderBuildManager, self).get_queryset().extra(
            where=('ord_no = CAST(%s as CHAR(8))',),
            params=(ord_no,))
        return qs

class OrderBuild(models.Model):
    ord_no = models.CharField(max_length=8)
    item_no = models.CharField(max_length=30)
    qty = models.DecimalField(max_digits=13, decimal_places=4, blank=True, null=True)
    qty_per = models.DecimalField(max_digits=15, decimal_places=6, blank=True, null=True)
    id = models.AutoField(db_column='ID', unique=True, primary_key=True)

    objects = OrderBuildManager()

    class Meta:
        managed = False
        db_table = 'imordbld_sql'

Вот мой views.py, где я использую два разных метода для потяните то же ord_no

views.py

from django.shortcuts import render
from .models import OrderBuild

def order_build_view(request, *args, **kwargs):

    if request.method == 'GET':
        order = kwargs.get('order')

        order_using_default_filter = OrderBuild.objects.filter(ord_no=order)
        order_using_extra_cast = OrderBuild.objects.get_order(order)

        context = {
            'order_1': order_using_default_filter,
            'order_2': order_using_extra_cast
        }

        return render(request, 'chartest/order.html', context)

Шаблон очень прост c и просто отображает два объекта OrderBuild на странице.

Вот что показывает django -debug-toolbar для двух запросов.

Как видите, первый запрос, использующий фильтр Django по умолчанию, значительно медленнее, чем второй. Я вижу эти результаты последовательно, и также не имеет значения, в каком порядке выполняются запросы. Также обратите внимание, что в этих примерах используется тестовая база данных, которая намного меньше / менее активна, чем наша производственная база данных. Я вижу, что первый запрос иногда занимает до 5000 мс, а второй - только 50-100 мс.

Используя профилировщик событий SQL SSMS, я обнаружил фактическое выполнение запросов:

Первый запрос с использованием значения по умолчанию Django filter

declare @p1 int  set @p1=NULL  exec sp_prepexec @p1 output,N'@P1 nvarchar(16)',N'SELECT [imordbld_sql].[ord_no], [imordbld_sql].[item_no], [imordbld_sql].[qty], [imordbld_sql].[qty_per], [imordbld_sql].[ID] FROM [imordbld_sql] WHERE [imordbld_sql].[ord_no] = @P1',N' 6807319'  select @p1

Второй запрос с CAST to CHAR (8)

declare @p1 int  set @p1=NULL  exec sp_prepexec @p1 output,N'@P1 nvarchar(16)',N'SELECT [imordbld_sql].[ord_no], [imordbld_sql].[item_no], [imordbld_sql].[qty], [imordbld_sql].[qty_per], [imordbld_sql].[ID] FROM [imordbld_sql] WHERE (ord_no = CAST(@P1 as CHAR(8)))',N' 6807319'  select @p1

Как видно из обоих запросов, параметр @ P1 объявлен как тип nvarchar (16), который несовместим с типом данных CHAR (8), которым должен быть ord_no. Когда SQL Сервер выполняет первый запрос, он использует сканирование индекса для получения результатов. Во втором запросе, поскольку я привел его к правильному типу данных, SQL может использовать поиск по индексу, который намного быстрее сканирования.

Мой вопрос: возможно ли заставить Django использовать тип данных CHAR (n) для CharFields в фильтре. Я не хочу создавать методы для всех моих моделей, которые используют тип данных CHAR (n) SQL - особенно, поскольку мне приходится использовать метод queryset.extra (), который устарел Django. Я также не уверен, является ли это проблемой Django или django -ms sql -обязанием, поэтому любая / вся помощь приветствуется.

Что я пробовал: I увидел этот вопрос , который несколько связан, поэтому я реализовал ответ, создав собственный тип поля. Я создал fields.py, который содержит пользовательский тип поля.

fields.py

from django.db.models import Field

class SQLCharField(Field):
    def __init__(self, length, *args, **kwargs):
        self.max_length = length
        super().__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'CHAR(%s)' % self.max_length

Затем я заменил CharField в своей модели на созданный мной SQLCharField :

models.py

from django.db import models
from .fields import SQLCharField

...


class OrderBuild(models.Model):
    ord_no = SQLCharField(length=8)
    item_no = SQLCharField(length=30)
    qty = models.DecimalField(max_digits=13, decimal_places=4, blank=True, null=True)
    qty_per = models.DecimalField(max_digits=15, decimal_places=6, blank=True, null=True)
    id = models.AutoField(db_column='ID', unique=True, primary_key=True)

    objects = OrderBuildManager()

    class Meta:
        managed = False
        db_table = 'imordbld_sql'

К сожалению, это не изменило способ выполнения запросов (параметр по-прежнему объявляется как nvarchar (16)) И если только я неправильно понимаю использование пользовательских типов данных, эта функциональность предназначена в первую очередь для создания миграций, не связанных с моей проблемой.

РЕДАКТИРОВАТЬ

Я сейчас использую метод raw () в наборе запросов, который позволяет мне написать запрос точно так, как необходимо для правильного использования индексов. К сожалению, я должен сделать это для всех моих моделей, но по крайней мере он не использует extra ()

class OrderBuildManager(models.Manager):


    def get_order(self, ord_no):
        return super(OrderBuildManager, self).get_queryset().raw(
        'SELECT ord_no, item_no, qty, qty_per, id FROM imordbld_sql WHERE ord_no 
        = %s', [ord_no])

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