Работа с базой данных в Joomla

JDatabaseQuery - использование union-методов в запросах

Joomla

Перед прочтением этого материала рекомендуем вам ознакомиться со следующими материалами:

Общая информация

Использование UNION при составлении запросов позволяет удобно комбинировать результаты двух или более SELECT запросов, которые необязательно соединены какими-либо отношениями.

Это также может быть полезной оптимизацией производительности. Использование UNION для объединения результата запросов иногда может быть намного быстрее, чем единичный запрос с WHERE, в особенности тогда, когда запрос включает в себя объединение с другими большими таблицами.

Для тех, кто хоть немного знаком с теорией набора - UNION делает именно то, что от него ожидают. Он сливает набор результатов из одного запроса с набором результатов другого запроса в один единый набор результатов.

Для того чтобы использовать UNION, вы должны быть знакомы с основными требованиями, предъявляемыми SQL-сервером, который вы используете. Они не зависят от Joomla, но если вы не будете с ними считаться, то получите ошибки базы данных. Например, SELECT запрос должен возвращать такое же самое число полей, в той же самой последовательности и с тем же самым типом данных.

Простой пример

Предположим, что вы хотите сделать рассылку группе людей, но их имена и адреса электронной почты находятся в разных таблицах. Давайте разберем конкретный пример. Вы хотите сделать рассылку клиентам и поставщикам, и их имена и адреса электронной почты находятся в таблицах customers и suppliers соответственно.

Этот запрос вытащит всю необходимую информацию по клиентам для рассылки:

$query
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__customers'));

А этот запрос сделает тоже самое по поставщикам:

$query
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__suppliers'));

Вы можете объединить это в один запрос следующим образом:

$query
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__customers'))
    ->union(
        $q2->select(
            $db->quoteName(
                array('name', 'email')
            )
        )
        ->from($db->quoteName('#__suppliers'))
    );
 
$mailshot = $db->setQuery($query)->loadObjectList();

На самом деле набор результатов, который получается при использовании union(), будет немного отличаться от отдельных запросов, потому что union() автоматически удалит дубликаты. Если же вы хотите, чтобы в результирующем наборе остались дубликаты (что в принципе с математической точки зрения уже не является набором), используйте метод unionAll() вместо union().

Разные варианты использования union

Методы union() и unionAll() довольно гибки при передаче в них аргументов. Вы можете передать просто строку запроса, объект JDatabaseQuery или массив объектов JDatabaseQuery. Предположим, что у вас есть три таблицы, похожие на пример выше:

$q1
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__customers'))
 
$q2
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__suppliers'))
 
$q3
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__ shareholders '));

Тогда все эти запросы дадут один и тот же результат:

// Метод union может стыковаться
$q1
->union($q2)
->union($q3);
 
// Метод union принимает строковые запросы
$q1
->union($q2)
->union('SELECT name, email FROM shareholders');
 
// Метод union принимает массив объектов JDatabaseQuery 
$q1->union(
    array($q2, $q3)
);
 
// Не важно, какой из объектов является корневым. В данном случае сам запрос будет немного другим, но результат будет таким же.
$q2->union(
    array($q1, $q3)
);

Использование UNION вместо OR

В некоторых случаях использование union() может привести к значительному приросту производительности по сравнению с использованием OR или WHERE вместе c IN.

Предположим, что у вас есть таблица продуктов, и вы хотите выделить только те продукты, которые принадлежат двум конкретным категориям. Обычно это делают так:

$query
    ->select('*')
    ->from($db->quoteName('#__products'))
    ->where(
        $db->quoteName('category')
        . ' = '
        . $db->quote('catA'), 'or')
    ->where(
        $db->quoteName('category')
        . ' = '
        . $db->quote('catB')
    );
 
$products = $db->setQuery($query)->loadObjectList();

Но, вполне вероятно, что вы увидите прирост производительности при использовании union():

$query
    ->select('*')
    ->from($db->quoteName('#__products'))
    ->where(
        $db->quoteName('category')
        . ' = '
        . $db->quote('catA')
);
 
$q2
    ->select('*')
    ->from($db->quoteName('#__products'))
    ->where(
        $db->quoteName('category')
        . ' = '
        . $db->quote('catB')

$query->union($q2);
 
$products = $db->setQuery($query)->loadObjectList();

Сортировка результата

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

Предположим, вы хотите отсортировать имена и адреса электронной почты из примера с рассылкой выше в алфавитном порядке:

$q2
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__suppliers'));
 
$query
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__customers'))
    ->union($q2)
    ->order($db->quoteName('name'));
 
$mailshot = $db->setQuery($query)->loadObjectList();

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

$q2
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__suppliers'))
    ->order($db->quoteName('name'));
 
$query
    ->select(
        $db->quoteName(
            array('name', 'email')
        )
    )
    ->from($db->quoteName('#__customers'))
    ->order($db->quoteName('name'))
    ->union($q2);
 
$mailshot = $db->setQuery($query)->loadObjectList();

Все дело в том, что ORDER BY в отдельном операторе SELECT никак не влияет на сортировку в результирующем наборе. Запрос выше синтаксически верен, но MySQL оптимизатор просто проигнорирует ORDER BY в запросе SELECT по suppliers, и будет применен ORDER BY из SELECT запроса по customers.

Чтобы обойти эту проблему, необходимо добавить дополнительную колонку к набору результата и сортировать по ней. Вот один из вариантов:

$q2
    ->select(
        $db->quoteName(
            array('name , email, 1 as sort_col')
        )
    )
    ->from($db->quoteName('#__suppliers'));
 
$query
    ->select(
        $db->quoteName(
            array('name , email, 2 as sort_col')
        )
    )
    ->from($db->quoteName('#__customers'))
    ->union($q2)
    ->order(
        $db->quoteName('sort_col')
        . ', ' 
        . $db->quoteName('name')
    );
 
$mailshot = $db->setQuery($query)->loadObjectList();

Расширенная сортировка

Однако возникают случаи, когда важно иметь возможность сортировки ORDER BY по отдельным запросам, которые не будут проигнорированы оптимизатором.

Предположим, вы хотите отправить специальное предложение для топ 10 клиентов и топ 5 поставщиков. Для этого примените условие LIMIT в комбинации с ORDER BY и используйте фиктивный запрос:

$q2
    ->select(
        $db->quoteName(
            array('name , email, 1 as sort_col')
        )
    ->from($db->quoteName('#__suppliers'))
    ->order($db->quoteName('turnover') .' DESC')
    ->setLimit(5);
 
$q1
    ->select(
        $db->quoteName(
            array('name , email, 2 as sort_col')
        )
    ->from($db->quoteName('#__customers'))
    ->order($db->quoteName('turnover') .' DESC')
    ->setLimit(10);
 
$query
    ->select(
        $db->quoteName(
            array('name , email, 0 as sort_col')
        )
    ->from($db->quoteName('#__customers'))
    ->where('1 = 0')
    ->union($q1)
    ->union($q2)
    ->order('sort_col, name');
    ->order(
        $db->quoteName('sort_col')
        . ', ' 
        . $db->quoteName('name')
    );
 
$mailshot = $db->setQuery($query)->loadObjectList();

Если не использовать фиктивный запрос, то сортировка и лимит будут добавлены к результирующему набору, а не к отдельному.

Оригинальная статья:
Dmitry Rekun
Работаю в банковской сфере, а с веб-разработкой (непосредственно с Joomla) столкнулся в 2007 году. Теперь это моё хобби и время от времени вторая работа. Какое-то время вёл свой блог, но решил попробовать работать в команде. И вот c 2012 года я здесь :)