Справка MySQL: оптимизация запроса на обновление, который устанавливает ранг в соответствии с порядком другого столбца

Привет, сообщество StackOverflow:

Ситуация

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

id | budget | cost | rank | rank_score  
 1 |   500  |  20  |   ?  |     ?  
 2 |   400  |  40  |   ?  |     ?  
 3 |   300  |  40  |   ?  |     ?

Таким образом, в этой таблице cost имеют наибольший вес при определении ранга, за ними следует budget. Таким образом, запись № 2 будет иметь более высокий ранг, запись № 3 будет второй, а запись № 1 — последней. Как видите, если две записи имеют одинаковые cost, то budget нарушает равенство.

Теперь, чтобы легко отслеживать такой «вес», я создаю столбец rank_score, который будет содержать конкатенацию cost и budget. Таким образом, rank_score для таблицы выше будет:

id | budget | cost | rank | rank_score  
 1 |   500  |  20  |   ?  |   20500   
 2 |   400  |  40  |   ?  |   40400  
 3 |   300  |  40  |   ?  |   40300  

Это rank_score можно заполнить так:

UPDATE table_name
SET rank_score = CONCAT(cost, budget);

Проблема

Пока все в порядке. Но теперь возникает проблема. Мне нужен столбец rank, состоящий только из целых чисел, для нескольких вещей, таких как сортировка, но, прежде всего, для показа пользователю ранга его записи. Конечно, этот столбец rank будет равен порядку убывания rank_scores. Но я не могу найти способ вычислить этот столбец rank в одном запросе на обновление без необходимости выполнять подзапросы, циклы в php и т. д.

Что я пробовал

Итак, сначала я пытался получить расчет rank_score, например:

SELECT id,
       CONCAT(cost, budget) AS rank_score
FROM table_name ;

а затем зациклить в php все эти rank_scores, только чтобы построить запрос, который выглядел так:

UPDATE table_name
SET rank_score = CASE id WHEN 1 THEN 20500 END,
    rank = CASE id WHEN 1 THEN 3 END
WHERE id IN (1) ;

... Конечно, этот пример запроса на обновление не является полным, так как он содержит больше предложений WHEN THEN END для каждой записи в таблице. Излишне говорить, что это уродливо, особенно когда вы ожидаете, что у вас будут тысячи и тысячи записей.

Итак, в заключение, у меня уже есть способ вычисления rank_score, но я также хочу вычислить rank (= порядок убывания рейтинга) в том же запросе или, по крайней мере, без этого сумасшедшего цикла php и предложений CASE WHEN THEN END.

Спасибо, что читаете и думаете об этом ;)

Уточнения

Уточнение того, что сказал @SJuan76: я не могу присвоить ранг через php, поскольку бывают случаи, когда пользователю будет показано фиксированное количество записей за раз (например, его пользовательская страница: SELECT * WHERE user_id = 333, которая может получить 1, 3 или 8 записей) и ему нужно знать ранг каждой записи. Присвоение рейтинга через php в этом случае не работает, потому что такой рейтинг будет относиться к выбранным записям, а не ко всем в таблице.


person YOMorales    schedule 01.07.2011    source источник
comment
Спасибо @ypercube. Мне было интересно, как отформатировать эти фиктивные таблицы для лучшего выравнивания. Спасибо еще раз.   -  person YOMorales    schedule 02.07.2011


Ответы (3)


Во-первых, я бы изменил budget, cost и rank_score на целое число или другой числовой тип данных и вместо

UPDATE table_name
SET rank_score = CONCAT(cost, budget) ;

Затем вы должны использовать:

UPDATE table_name
SET rank_score = cost * 1000 + budget * 1  ;

Тогда это проще, так как вам не придется иметь дело со строковыми функциями и иметь что-то вроде:

SELECT * 
FROM table_name
WHERE (conditions...)
ORDER BY rank_score DESC

(Скобка: установка одного параметра (1000) выше другого (1) эквивалентна порядку cost, budget. Попробуйте это, чтобы проверить:

SELECT * 
FROM table_name
ORDER BY cost DESC
       , budget DESC

Так что вполне можно вообще отказаться от rank_score, если, конечно, вы не планируете проводить эксперименты с различными значениями параметров.

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

SELECT id, budget, cost, 
       cost*1000 + budget*1 AS rank_score_calculated
FROM table_name
ORDER BY rank_score_calculated DESC

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

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

Другой случай - это когда нужно абсолютное rank во всех строках таблицы, как вам нужно. Поскольку в MySQL нет "оконных" функций, очень сложно написать такой запрос на чистом SQL.)


Рейтинг можно рассчитать с помощью переменных MySQL.

SELECT *
     , @rownum:=@rownum+1 AS rank_calculated
FROM table_name
   , (SELECT @rownum:=0) AS st
ORDER BY rank_score DESC

И если вы хотите поместить эти значения в rank, используйте:

UPDATE table_name
         JOIN
         ( SELECT id
                , @rownum:=@rownum+1 AS rank_calculated
           FROM table_name
              , (SELECT @rownum:=0) AS st
           ORDER BY rank_score DESC
         ) AS r
         ON r.id = table_name.id
SET table_name.rank = r.rank_calculated ;

Приведенные выше два запроса не являются чистым SQL. Вы можете изучить возможность перехода на другую систему баз данных, которая поддерживает оконные функции, такие как Postgres, SQL-Server или Oracle.

person ypercubeᵀᴹ    schedule 01.07.2011
comment
Большое спасибо. Мне нужно многое переварить! :P Я начал тестировать часть, где я больше всего застрял (расчет ранга) и уже вижу положительные результаты. Позвольте мне продолжить работу с вашего ответа. - person YOMorales; 02.07.2011
comment
Итак, я последовал вашему ответу, и вот некоторые дополнительные сведения: 1) у меня уже были столбцы в виде десятичных знаков, и я имел дело с числовыми функциями/вычислениями, очень похожим на то, что вы показали. Я опустил эту деталь для краткости/простоты. 2) Я сохраняю поле rank_score по причинам, о которых вы говорите: запрос выполняется очень часто, таблица большая, обновляется довольно часто. 3) Ваше предложение использовать переменные MySQL для расчета и установки рейтинга работает отлично. Только это решает мой первоначальный ответ. - person YOMorales; 02.07.2011

Вы пробовали разделить его на два запроса? Или с помощью подзапроса?

mysql> select p.*, (select count(0)+1 from table_name as s where s.cost >= p.cost and s.budget < p.budget) as rank from table_name as p where p.id in (1,2,3);
+----+------+--------+------+
| id | cost | budget | rank |
+----+------+--------+------+
|  1 |   20 |    500 |    3 |
|  2 |   40 |    400 |    2 |
|  3 |   40 |    300 |    1 |
+----+------+--------+------+
3 rows in set (0.00 sec)
person Nthalk    schedule 01.07.2011
comment
Я думал разделить на два запроса. Позвольте мне немного подумать/проверить ваш ответ. - person YOMorales; 02.07.2011
comment
Кажется, работает для значений, которые я разместил здесь, но для многих других значений это не работает (иногда несколько записей имеют одинаковый ранг 1). Продолжим испытания. - person YOMorales; 02.07.2011

SQL уже может упорядочивать записи по вашему желанию, не требуя дополнительного табличного пространства и (что гораздо важнее) не нарушая нормальные формы.

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

Почему вы хотите сделать это в базе данных?

person SJuan76    schedule 01.07.2011
comment
Спасибо, что попросили дать дополнительные разъяснения. Вы правы, полагая, что я могу присвоить целочисленный ранг в программном коде, возможно, после выполнения SELECT * ORDER BY rank_score DESC;. Но это отлично работает, когда я извлекаю все записи. Бывают случаи, когда пользователю будет показано фиксированное количество записей за раз (например, его страница пользователя: SELECT * WHERE user_id = 333, которая может получить 1, 3 или 8 записей), и ему нужно знать, каков ранг каждой записи. Присвоение рейтинга через php в этом случае не работает, потому что такой рейтинг будет относиться к выбранным записям, а не ко всем в таблице. - person YOMorales; 02.07.2011