Почему и как запрос Django filter-by BooleanField переводится в SQL WHERE или WHERE NOT, а не 1/0?

Я заметил, что запрос, который раньше был быстрым в старой версии Django, теперь намного медленнее в 4.0.8.

Имеется достаточно большая таблица с FK 'marker' и boolean 'flag', к которой привязан индекс. Следующие запросы могут разумно вернуть десятки тысяч строк.

В моей кодовой базе есть запрос вида

MyModel.objects.filter(marker_id=123, flag=False).count()

В Django Debug Toolbar (а также в shell, когда я исследую str(qs.query)) это теперь разрешается следующим синтаксисом SQL:

SELECT ••• FROM `myapp_mymodel` WHERE (`myapp_mymodel`.`marker_id` = 123 AND NOT `myapp_mymodel`.`flag`)

В крайних случаях этот запрос выполняется в течение 20 секунд или около того. Между тем, в старой версии Django (1.11+) этот же запрос превращается в следующий SQL:

SELECT ••• FROM `myapp_mymodel` WHERE (`myapp_mymodel`.`marker_id` = 123 AND `myapp_mymodel`.`flag` = 0)

Это работает, так как схема таблицы содержит 'flag' как TINYINT(1), но самое главное, это работает намного быстрее - возврат происходит менее чем за секунду.

EDIT: Я попросил sql server ВЫПОЛНИТЬ оба запроса, и есть разница в том, что 'flag' появляется как потенциальный ключ в последнем (более быстром) запросе, но не в более медленном. Это согласуется с этим ответом, в котором говорится, что mysql должен видеть сравнение со значением, чтобы знать, что нужно использовать индекс. Таким образом, главный вопрос заключается в том, как я могу применить синтаксис, который использует уже имеющийся индекс?

END EDIT

Оригинальные вопросы: Почему существует разница в переводе ORM на SQL, и где я могу найти ответственный за это код (я проверил db.backends.mysql безрезультатно, или не смог распознать виновника)? Есть ли способ намекнуть Django, что я бы предпочел поведение "равно-ноль"?

Единственный обходной путь, который я вижу до сих пор, - это использование необработанного SQL-запроса. Я бы предпочел избежать этого, если это возможно.

Это регрессия, о которой уже сообщалось и которая была решена в трекере проблем Django в виде проблемы #32691.

Это исправлено в Django 4.1, так что если вы обновитесь, проблема будет решена автоматически. Для версий от 3.2 до 4.0 вы можете использовать обходное решение, отмеченное Тодором Величковым в этой проблеме, которое заключается в использовании Value() выражений:

from django.db.models import Value


MyModel.objects.filter(marker_id=123, flag=Value(0)).count()

(MySQL/MariaDB, используя ENGINE InnoDB)

Зависит от того, каков индекс(ы).

INDEX(flag)
<
INDEX(flag, ...)
, скорее всего, будет использоваться только в том случае, если совпадает менее 20% строк. (Число 20% является приблизительным.)

где дополнительные столбцы могут быть использованы для фильтрации, скорее всего, будет использоваться либо TRUE, либо FALSE.

Обратите внимание, что в InnoDB столбец(ы) PRIMARY KEY's неявно прикрепляется к концу индекса. Это превращает INDEX(flag) в INDEX(flag, id), возможно, позволяя использовать вторую форму выше.

Вернуться на верх