Оптимизация ORDER BY, когда результирующий набор очень велик и его нельзя упорядочить по индексу

Как сделать так, чтобы предложение ORDER BY с небольшим LIMIT (т. е. 20 строк за раз) возвращалось быстро, когда я не могу использовать индекс для удовлетворения порядка строк?

Допустим, я хотел бы получить определенное количество заголовков из «узла» таблицы (упрощенное ниже). Я использую MySQL, кстати.

node_ID INT(11) NOT NULL auto_increment,
node_title VARCHAR(127) NOT NULL,
node_lastupdated INT(11) NOT NULL,
node_created INT(11) NOT NULL

Но мне нужно ограничить возвращаемые строки только теми, к которым имеет доступ конкретный пользователь. Многие пользователи имеют доступ к большому количеству узлов. У меня есть эта информация, предварительно рассчитанная в большой таблице поиска (попытка упростить задачу), где первичный ключ охватывает оба столбца, а наличие строки означает, что группа пользователей имеет доступ к этому узлу:

viewpermission_nodeID INT(11) NOT NULL,
viewpermission_usergroupID INT(11) NOT NULL

Поэтому мой запрос содержит что-то вроде

FROM
  node
  INNER JOIN viewpermission ON
    viewpermission_nodeID=node_ID
    AND viewpermission_usergroupID IN (<...usergroups of current user...>)

... и я также использую GROUP BY или DISTINCT, чтобы узел возвращался только один раз, даже если две пользовательские «группы пользователей» имеют доступ к этому узлу.

Моя проблема заключается в том, что для предложения ORDER BY, которое сортирует результаты по дате создания или последнего обновления, нет возможности использовать индекс, потому что возвращаемые строки зависят от значений в другой таблице разрешений на просмотр.

Поэтому MySQL должен будет найти все строки, которые соответствуют критериям, а затем отсортировать их все самостоятельно. Если для конкретного пользователя существует миллион строк, и мы хотим просмотреть, скажем, последние 100 или строки 100-200 при упорядочении по последнему обновлению, БД потребуется выяснить, какой миллион строк может видеть пользователь, отсортировать весь этот набор результатов, прежде чем он сможет вернуть эти 100 строк, верно?

Есть ли какой-нибудь творческий способ обойти это? Я думал в том же духе:

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

Изменить: упрощенный вопрос

Возможно, я смогу упростить вопрос, переписав его так:

Есть ли способ переписать этот запрос или создать индекс для следующего, чтобы индекс можно было использовать для упорядочения (а не только для выбора строк)?

SELECT nodeid
FROM lookup
WHERE
  usergroup IN (2, 3)
GROUP BY
  nodeid

Индекс (группа пользователей) позволяет части WHERE удовлетворяться индексом, но GROUP BY принудительно использует временную таблицу и сортировку файлов для этих строк. Индекс на (nodeid) ничего для меня не делает, потому что предложению WHERE нужен индекс с группой пользователей в качестве первого столбца. Индекс по (usergroup, nodeid) вызывает временную таблицу и сортировку файлов, поскольку GROUP BY не является первым столбцом индекса, который может изменяться.

Любые решения?


person thomasrutter    schedule 26.02.2009    source источник
comment
К сожалению, правильный ответ - добавить индекс. Пожалуйста, объясните, почему это не вариант.   -  person paxdiablo    schedule 26.02.2009
comment
Мне не удалось придумать какой-либо способ организации индекса, который позволил бы мне упорядочивать строки и использовать этот индекс для сортировки (а не только ГДЕ). Если вы знаете об одном, дайте мне знать.   -  person thomasrutter    schedule 26.02.2009


Ответы (4)


Могу я ответить на свой вопрос?

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

Чтобы выбрать упрощенный пример, вместо этого:

SELECT id FROM ids WHERE groups IN(1,2) ORDER BY id

Если вам нужно использовать индекс как для выбора строк, так и для их упорядочения, вы должны абстрагировать этот IN(1,2), чтобы он был постоянным, а не диапазоном, т.е.:

SELECT id FROM ids WHERE grouplist='1,2' ORDER BY id

Конечно, вместо использования строки «1,2» у вас может быть внешний ключ и т. д. Дело в том, что вам нужно будет иметь строку не только для каждой группы, но и для каждой комбинации нескольких групп.

Итак, вот мой ответ.

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

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

SELECT
    <fields>
FROM
  node
  INNER JOIN viewpermission ON
    viewpermission_nodeID=node_ID
    AND viewpermission_usergroupID IN (<...usergroups of current user...>)
  FORCE INDEX(node_created_and_node_ID)
GROUP BY
  node_created, node_ID

GROUP BY может использовать индекс, если он начинается с крайнего левого столбца индекса и находится в первой неконстантной несистемной таблице для обработки. Затем объединение работает со всем списком (который уже упорядочен), и только те, которые не видны текущему пользователю (которых будет небольшая часть), удаляются с помощью ВНУТРЕННЕГО СОЕДИНЕНИЯ.

person thomasrutter    schedule 26.02.2009
comment
Да, вы можете ответить на свой вопрос. Вы даже можете принять его через два дня. - person Matthew Farwell; 26.02.2009
comment
@thomasrutter: я проверил то, что вы предлагаете, в MySQL 5.0.75. Он отказывается использовать индекс, пока я не скажу FORCE INDEX вместо USE INDEX. Но тогда он избавляется от сортировки файлов. - person Bill Karwin; 26.02.2009
comment
Спасибо, Билл, да, похоже, что FORCE INDEX иногда необходим, это немного хакерски, и я надеялся, что MySQL увидит преимущество использования индекса для сортировки, когда таблицы вырастут, но, похоже, нам нужно, чтобы мы показали ему праведный путь. - person thomasrutter; 27.02.2009

Скопируйте значение, которое вы собираетесь заказывать, в таблицу разрешений на просмотр и добавьте его в свой индекс.

Вы можете использовать триггер для сохранения этого значения из другой таблицы.

person bobwienholt    schedule 26.02.2009
comment
Мое исследование того, как оптимизируется ORDER BY, говорит мне, что, поскольку выбор группы пользователей является не константой, а диапазоном IN(), он все равно не сможет использовать индекс для ORDER BY. То есть группа пользователей WHERE IN (...) ORDER BY sortorder не может использовать индекс для сортировки. Это правда? - person thomasrutter; 26.02.2009

select * from
(
select *
FROM  node  
INNER JOIN viewpermission 
ON    viewpermission_nodeID=node_ID    
AND viewpermission_usergroupID IN (<...usergroups of current user...>)
) a
order by a.node_lastupdated desc

Внутренний запрос дает вам отфильтрованное подмножество, которое, как я понимаю, существенно меньше, чем весь набор. Только меньшие должны быть отсортированы.

person cdonner    schedule 26.02.2009
comment
Это хорошее решение, примерно эквивалентное тому, что я использую сейчас, но, к сожалению, мне это нужно, чтобы не задохнуться, когда отфильтрованное подмножество все еще очень велико. Для этого я считаю, что ORDER BY должен использовать индекс для упорядочения, а не только для выбора подмножества. - person thomasrutter; 26.02.2009

У MySQL возникают проблемы, когда вы используете GROUP BY и ORDER BY в одном запросе. Это вызывает сортировку файлов, и это, вероятно, самый большой штраф за производительность.

Вы можете устранить необходимость в DISTINCT (или GROUP BY), используя некоррелированный подзапрос вместо JOIN.

SELECT * FROM node
WHERE node_id IN (
  SELECT viewpermission_nodeID
  FROM viewpermission
  WHERE viewpermissiong_usergroupID IN ( <...usergroups...> )
)
ORDER BY node_lastupdated DESC
LIMIT 100;

Нет необходимости сортировать или выполнять DISTINCT в подзапросе, поскольку IN (1, 1, 2, 3) совпадает с IN (1, 3, 2).

Обратите внимание, что MySQL может использовать только один индекс для каждой таблицы в заданном запросе, поэтому он попытается сделать лучший выбор между индексом по node_id и индексом по node_lastupdated. Он не может использовать оба, и даже если вы сделаете составной индекс, это не поможет в этом случае.

Не забудьте проанализировать различные решения с помощью EXPLAIN.

person Bill Karwin    schedule 26.02.2009
comment
Я понимаю, что ORDER BY во внешнем предложении select по-прежнему не сможет использовать здесь индекс, потому что предложение WHERE использует диапазон IN(), а не константу. Если внутренний выбор возвращает много идентификаторов, это все равно будет медленным. Однако постараюсь больше поэкспериментировать с EXPLAIN. - person thomasrutter; 26.02.2009