SQLAlchemy и пустой оператор IN

Я узнал, что SQLAlchemy переводит

db.query(...).filter(A.id.in_(ids))

в

SELECT ...
FROM a
WHERE a.id != a.id

если ids пусто. Это приводит к последовательному сканированию в таблице a, что, очевидно, катастрофично для производительности.

Первый вопрос: почему? Почему не просто 1 = 0 или что-то, что не требует последовательного сканирования?

Второй, более важный: есть ли обычно обходное решение (кроме if рядом с каждым in_)?

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

ИЗМЕНИТЬ

SQLAlchemy регистрирует предупреждение каждый раз, когда это происходит:

"The IN-predicate on 'foo.bar' was invoked with an empty sequence. This results in a contradiction, which nonetheless can be expensive to evaluate. Consider alternative strategies for improved performance."

Ответы

Ответ 1

Я использую:

if len(ids) > 0:
    db.query(...).where(A.id.in_(ids))
else:
    db.query(...).where(False)

Я попробовал .limit(0) вместо .where(false) без успеха. В пустых запросах есть несколько заглавных различий, которые разрушали другие вещи по конвейеру. Этот обходной путь, хотя и может быть быстрее, по крайней мере позволяет избежать упомянутого предупреждения.

Ответ 2

Знайте, о чем вы просите:

  • Только если значение A.id сопоставимо, любое сравнение действительно может быть выполнено. Невысокое значение не сравнимо ни с чем, все сравнения приведут к несуществующему значению, которое, в свою очередь, оценивается как False. То есть, если A.id равно NULL, тогда A.ID == anything является False, а A.ID != anything также является False: A.ID == A.ID || A.ID != A.ID является False, если A.id - NULL.
  • IN -clause с пустой последовательностью спрашивает, является ли это значение частью пустого списка. Не существующее значение является частью списка, даже пустого.
  • Следовательно, то, о чем вы просите, это некоторые изменения IS NOT NULL и того, что является частью ничего. Это условие, для которого необходимо проверить. Ненужное значение не является чем-то; только некоторое значение, которое не является NULL, может не быть членом пустого списка...
  • Поскольку sqlalchemy умна в том, что это, вероятно, не так, как вы хотите выразить это условие, оно дает предупреждение. Вероятно, вы должны отказаться от IN -clause, если последовательность пуста.

Для конкретного примера это sqlfiddle

Для более философского подхода см. Какова природа пустоты

Ответ 3

Когда я столкнулся с этим, это произошло потому, что я использовал тип Enum для одного из столбцов таблицы базы данных. Когда я изменил его на String, проблема исчезла. Это не настоящее решение, так как я предпочел бы Enum, но он действительно избежал проблемы.

Ответ 4

Чтобы ответить на вопрос OP о "почему", вот FAQ в записи (что всегда удивительно сложно найти):

Почему .col.in_([]) создает col != col? Почему бы не 1=0?

Небольшое введение в проблему. Оператор IN в SQL, учитывая список элементов для сравнения с столбцом, как правило, не принять пустой список, то есть в то время как верно:

column IN (1, 2, 3)

его недействительно сказать:

column IN ()

SQLAlchemy Operators.in_() оператор, заданный пустым списком, выдает это выражение:

column != column

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

Например, наивный подход "просто оценивается как ложный, сравнивая 1 = 0 или 1!= 1", неправильно обрабатывает нули. Выражение как:

NOT column != column

не возвращает строку, если column IS NULL, но выражение, которое не учитывает столбец, например:

NOT 1=0

вернет строку.

Как показано в этом сообщении, вы можете использовать функцию ANY, чтобы избежать этого, поскольку он синтаксически действителен даже для пустого списка (но не поддерживается на SQLite, по-видимому). Это, вероятно, быстрее и для больших списков, поскольку для построения запроса он меньше строит строку.

Проблема производительности с оператором in_ была недавно исправлена ​​, и исправление, вероятно, будет в SQLAlchemy 1.2.0.

Ответ 5

использовать подзапрос, если ids пуст, будет (никогда не исполняется).

Пример:

subquery = db.query(SomeTable.id).filter(...).subquery()

db.query(...).filter(A.id.in_(subquery))

в

SELECT ...
FROM a
WHERE a.id IN (SELECT ...)