Почему и как запрос 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)
, возможно, позволяя использовать вторую форму выше.