Выберите значения, которые соответствуют различным условиям в разных строках? - PullRequest
29 голосов
/ 25 января 2009

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

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

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

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

Ответы [ 6 ]

113 голосов
/ 25 января 2009

Хорошо, я получил отрицательный ответ по этому поводу, поэтому я решил проверить его:

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

CREATE INDEX ON userrole (roleid);

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

<code><?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 "
\ п "; запрос функции ($ str) { mysql_query ($ строка); if (mysql_error ()) { echo "$ str:". mysql_error (). "\ П"; } } ?>

Выход:

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 с

Это верно. Предлагаемая мною версия соединения * в 1022 раза быстрее, чем агрегатная версия.

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

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

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

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

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

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

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

25 голосов
/ 25 января 2009
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 и посмотрите «Использование индекса» в примечаниях для всех трех таблиц, что означает, что он читает индекс и даже не должен касаться строк данных.

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

4 голосов
/ 08 августа 2009

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

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

Я предполагаю, что у вас есть таблица 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
  )
);

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

3 голосов
/ 25 января 2009

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

select count(*), userid from t
where roleid in (1,2,3)
group by userid
having count(*) = 3
1 голос
/ 26 ноября 2010
select userid from userrole where userid = 1
intersect
select userid from userrole where userid = 2
intersect
select userid from userrole where userid = 3

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

0 голосов
/ 25 января 2009

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

...