Выбрать значения, соответствующие разным условиям в разных строках?

StackOverflow https://stackoverflow.com/questions/477006

Вопрос

Это очень простой запрос, который я не могу понять....

Допустим, у меня есть таблица с двумя столбцами, например:

userid  |  roleid
--------|--------
   1    |    1
   1    |    2
   1    |    3
   2    |    1

Я хочу получить все отдельные идентификаторы пользователей, которые имеют roleids 1, 2 И 3.Используя приведенный выше пример, единственный результат, который я хочу вернуть, это userid 1.Как мне это сделать?

Это было полезно?

Решение

SELECT userid
FROM UserRole
WHERE roleid IN (1, 2, 3)
GROUP BY userid
HAVING COUNT(DISTINCT roleid) = 3;
<Ч>

Всем, кто читает это: мой ответ прост и понятен и получил статус «принят», но, пожалуйста, прочитайте ответ , данный @cletus. У него гораздо лучшая производительность.

<Ч>

Просто размышляя вслух, еще один способ написать самообъединение, описанное @cletus:

SELECT t1.userid
FROM userrole t1
JOIN userrole t2 ON t1.userid = t2.userid
JOIN userrole t3 ON t2.userid = t3.userid
WHERE (t1.roleid, t2.roleid, t3.roleid) = (1, 2, 3);

Это может быть проще для вас, и MySQL поддерживает сравнение таких кортежей. MySQL также знает, как разумно использовать индексы покрытия для этого запроса. Просто запустите его через EXPLAIN и посмотрите & Quot; Использование индекса & Quot; в примечаниях для всех трех таблиц, что означает, что он читает индекс и даже не должен касаться строк данных.

Я выполнил этот запрос на 2,1 млн строк (дамп данных переполнения стека за июль для PostTags), используя MySQL 5.1.48 на моем Macbook, и он возвратил результат за 1,08 с. На приличном сервере с достаточным объемом памяти, выделенным для innodb_buffer_pool_size, это должно быть еще быстрее.

Другие советы

Хорошо, меня за это проголосовали, поэтому я решил проверить это:

CREATE TABLE userrole (
  userid INT,
  roleid INT,
  PRIMARY KEY (userid, roleid)
);

CREATE INDEX ON userrole (roleid);

Запустите это:

<?php
ini_set('max_execution_time', 120); // takes over a minute to insert 500k+ records 

$start = microtime(true);

echo "<pre>\n";
mysql_connect('localhost', 'scratch', 'scratch');
if (mysql_error()) {
    echo "Connect error: " . mysql_error() . "\n";
}
mysql_select_db('scratch');
if (mysql_error()) {
    echo "Selct DB error: " . mysql_error() . "\n";
}

$users = 200000;
$count = 0;
for ($i=1; $i<=$users; $i++) {
    $roles = rand(1, 4);
    $available = range(1, 5);
    for ($j=0; $j<$roles; $j++) {
        $extract = array_splice($available, rand(0, sizeof($available)-1), 1);
        $id = $extract[0];
        query("INSERT INTO userrole (userid, roleid) VALUES ($i, $id)");
        $count++;
    }
}

$stop = microtime(true);
$duration = $stop - $start;
$insert = $duration / $count;

echo "$count users added.\n";
echo "Program ran for $duration seconds.\n";
echo "Insert time $insert seconds.\n";
echo "</pre>\n";

function query($str) {
    mysql_query($str);
    if (mysql_error()) {
        echo "$str: " . mysql_error() . "\n";
    }
}
?>

Выход:

499872 users added.
Program ran for 56.5513510704 seconds.
Insert time 0.000113131663847 seconds.

Это добавляет 500 000 случайных комбинаций ролей пользователей, и примерно 25 000 соответствуют выбранным критериям.

Первый запрос:

SELECT userid
FROM userrole
WHERE roleid IN (1, 2, 3)
GROUP by userid
HAVING COUNT(1) = 3

Время запроса:0,312 с

SELECT t1.userid
FROM userrole t1
JOIN userrole t2 ON t1.userid = t2.userid AND t2.roleid = 2
JOIN userrole t3 ON t2.userid = t3.userid AND t3.roleid = 3
AND t1.roleid = 1

Время запроса:0,016 с

Это верно.Предложенная мной версия объединения: в двадцать раз быстрее агрегатной версии.

Извините, но я зарабатываю этим на жизнь и работаю в реальном мире, а в реальном мире мы тестируем SQL, и результаты говорят сами за себя.

Причина этого должна быть довольно ясна.Стоимость агрегированного запроса будет масштабироваться в зависимости от размера таблицы.Каждая строка обрабатывается, агрегируется и фильтруется (или нет) через HAVING пункт.Версия объединения будет (с использованием индекса) выбирать подмножество пользователей на основе заданной роли, затем проверять это подмножество по второй роли и, наконец, это подмножество по третьей роли.Каждый выборреляционная алгебра терминах) работает со все более малым подмножеством.Из этого можно сделать вывод:

Производительность версии с объединением становится еще лучше за счет меньшего количества совпадений.

Если бы было только 500 пользователей (из приведенной выше выборки 500 тысяч) с тремя указанными ролями, версия с присоединением будет работать значительно быстрее.Агрегированная версия не будет (и любое улучшение производительности является результатом транспортировки 500 пользователей вместо 25 тысяч, которые, очевидно, получает и объединенная версия).

Мне также было любопытно посмотреть, как с этим справится реальная база данных (т. е. Oracle).Итак, я повторил то же упражнение на Oracle XE (работающем на том же настольном компьютере с Windows XP, что и MySQL из предыдущего примера), и результаты почти идентичны.

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

Обновлять: После некоторого обширное тестирование, картина сложнее и ответ будет зависеть от ваших данных, вашей базы данных и других факторов.Мораль этой истории такова: тест, тест, тест.

Классический способ сделать это - рассматривать это как проблему реляционного разделения.

На английском языке: выберите тех пользователей, для которых отсутствует ни одно из требуемых значений ролей.

Я предполагаю, что у вас есть таблица Users, к которой относится таблица UserRole, и я предполагаю, что нужные значения roleid находятся в таблице:

create table RoleGroup(
  roleid int not null,
  primary key(roleid)
)
insert into RoleGroup values (1);
insert into RoleGroup values (2);
insert into RoleGroup values (3);

Я также предполагаю, что все соответствующие столбцы не имеют значения NULL, поэтому никаких сюрпризов с IN или NOT EXISTS нет. Вот SQL-запрос, который выражает английский выше:

select userid from Users as U
where not exists (
  select * from RoleGroup as G
  where not exists (
    select R.roleid from UserRole as R
    where R.roleid = G.roleid
    and R.userid = U.userid
  )
);

Еще один способ написать это это

select userid from Users as U
where not exists (
  select * from RoleGroup as G
  where G.roleid not in (
    select R.roleid from UserRole as R
    where R.userid = U.userid
  )
);

Это может быть или не быть эффективным, в зависимости от индексов, платформы, данных и т. д. Поищите в сети " реляционное деление " и вы найдете много.

Предполагая, что ID пользователя, roleid содержатся в уникальном индексе (то есть не может быть 2 записи, где userid = x и roleid = 1

select count(*), userid from t
where roleid in (1,2,3)
group by userid
having count(*) = 3
select userid from userrole where userid = 1
intersect
select userid from userrole where userid = 2
intersect
select userid from userrole where userid = 3

Разве это не решит проблему? Насколько хорошо это решение для типичных реляционных БД? Будет ли оптимизатор запросов автоматически оптимизировать это?

Если вам нужна какая-либо общность здесь (разные комбинации с 3 ролями или комбинации с n ролями) ... Я бы предложил вам использовать систему битовой маскировки для ваших ролей и использовать побитовые операторы для выполнения ваших запросов. ..

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top