Существует два основных подхода к этой проблеме: 1) динамическая генерация оператора SQL, 2) обработка NULL с выражениями в статической инструкции SQL
Динамический подход, если запрос выполняется из приложения ...
мы начинаем со статической строки, как будет выглядеть запрос каждый раз:
$sql = 'SELECT t.id
FROM mytable t
WHERE 1=1';
и затем решить, собираемся ли мы добавить еще одно условие в предложение WHERE
if( $p1 !== '' ) {
$sql .= ' AND t.propertyA = :p1';
}
if( $p2 !== '' ) {
$sql .= ' AND t.propertyB = :p2';
}
// prepare the SQL statement
$sth=$pdo->prepare($sql);
// conditionally bind values to the placeholders
if( $p1 !== '' ) {
$sth->bindValue(':p1',$p1);
}
if( $p2 !== '' ) {
$sth->bindValue(':p2',$p2);
}
Недостатком динамического SQL является то, что в случае более сложных проблем правильное построение оператора SQL может оказаться слишком сложным. При наличии только условий в предложении WHERE
это жизнеспособный подход.
Другим недостатком является большое разнообразие SQL-операторов, которые мы можем в конечном итоге создать, и обеспечение того, что у каждого варианта будет подходящий план выполнения, становится сложным. Имея только два необязательных условия в предложении WHERE, мы получаем очень легко управляемые 4 варианта ...
О, причина включения условия 1=1
в предложение WHERE
не влияет на утверждение; оптимизатор достаточно умен, чтобы понять, что это верно для каждой возможной строки, так что условие сбрасывается. Что нас покупает, так это когда мы добавляем к предложению WHERE
, мы избавляемся от необходимости проверять «это первое условие в предложении WHERE?» поэтому мы знаем, добавлять ли WHERE
или AND
к утверждению.
Второй подход - использовать статический с SQL, используя некоторые выражения.
В качестве примера предположим, что propertyA и PropertB являются столбцами символьного типа:
$sql = "SELECT t.id
FROM mytable t
WHERE t.propertyA <=> IFNULL(NULLIF(:p1,''),t.propertyA)
AND t.propertyB <=> IFNULL(NULLIF(:p2,''),t.propertyB)";
$sth = $pdo->prepare($sql);
$sth->bindValue(':p1',$p1);
$sth->bindValue(':p2',$p2);
Если мы предоставим ненулевую строку для :p1
, то функция NULLIF
вернет :p1
, а функция IFNULL вернет :p1
. Это будет, как если бы мы только что написали:
t.propertyA <=> :p1
Если мы предоставим строку нулевой длины для $p1
(для заполнителя :p1
), то функция SQL NULLIF
вернет NULL. И, в свою очередь, функция IFNULL
вернет t.propertyA
, поэтому оператор будет сравнивать propertyA
с самим собой, поэтому чистый результат будет таким, как мы написали
AND t.propertyA <=> t.propertyA
или просто
AND 1=1
(Разница в том, что оптимизатор не откажется от нашего условия, так как оптимизатор не знает, какое значение мы собираемся предоставить: p1, когда подготовлен план выполнения.
ПРИМЕЧАНИЕ. Оператор космического корабля <=>
является NULL-безопасным сравнением. Он гарантированно вернет TRUE или FALSE (а не вернет NULL), в отличие от стандартного сравнения на равенство (=
), которое возвращает NULL, когда любое (или оба) из сравниваемых значений равно NULL.
Это:
foo <=> bar
по существу означает сокращение для эквивалента:
foo = bar OR ( foo IS NULL AND bar IS NULL )
Если мы гарантируем, что propertyA
никогда не будет NULL (например, с помощью явного ограничения NOT NULL в определении таблицы), мы можем отказаться от оператора космического корабля и просто использовать сравнение с простым равенством.
Недостатком этого подхода является менее интуитивный оператор SQL; непосвященные могут царапать голову над ним. Поэтому мы хотим оставить комментарий в коде, объясняющий, что условием сопоставления является условие, если :p1
- пустая строка, сравнение с :p1
.
невозможно.
Мы могли бы использовать другой синтаксис для достижения того же результата, например, используя более портативную функцию COALESCE
, соответствующую стандартам ANSI, вместо IFNULL
и выражение CASE
вместо NULLIF
. (Это потребовало бы, чтобы мы поставили $p1
дополнительному заполнителю, написав это так, как мы, мы должны предоставить $p1
только один раз.)
Но это две основные модели. Выбери свой яд.