
Недавно пришлось выполнить махинацию с БД которая, как кажется на первый взгляд, совершенно невыполнима средствами MySQL. Перед глазами у меня была таблица товаров, сортировка которых осуществляется вспомогательным столбцом `order_num` ('порядковый номер'): она позволяет задавать ручную сортировку товаров.
Но вот потребовалось автоматически заполнить этот столбец так, чтобы товары оказались отсортированы по названию: то есть, с рядом ограничений, изменить столбец `order_num` во всей таблице. Очень хотелось обойтись средствами MySQL без привлечения каких-либо дополнительных инструментов, и задача была решена :)
Сложность задачи также в том, что MySQL не умеет делать UPDATE таблицы и одновременно читать из неё: в MyISAM таблица эксклюзивно блокируется при записи и нет возможности произвести чтение в подзапросе.
Задача
есть таблица товаров обычного интернет-магазина:
CREATE TABLE `products` (
`product_id` INT NOT NULL PRIMARY KEY COMMENT 'id товара',
`model` VARCHAR(255) NOT NULL COMMENT 'название товара',
`order_num` INT NOT NULL COMMENT 'номер товара при сортировке',
) COMMENT 'товары';
* This source code was highlighted with Source Code Highlighter.
Изначально товары отсортированы чёрт знает как. Нужно отсортировать их в базе по `model`: то есть чтобы столбец сортировки `order_num` товаров увеличивался параллельно сортировке по `model`.
В результате таблица должна выглядеть так:
| `product_id` |
`model` |
`order_num` |
| 70 |
Ааа |
10 |
| 10 |
Ббб |
20 |
| 30 |
Ввв |
30 |
| 20 |
Ггг |
70 |
Движок этого магазина глючит если у нескольких товаров `order_num` совпадает. Сортировать товары по `model` при выводе нельзя: потеряется возможность сортировки вручную.
Идея
В столбце сортировки `order_num` должны стоять те же id товаров `product_id`, но в правильном порядке. Так мы точно избежим дублирования значений сортировки, а «ORDER BY `order_num`» выведет товары, отсортированные по `model` — то есть правильно :)
Замечу, что в частном случае проводилась сортировка товаров одной категории, поэтому нельзя использовать порядковые 0,1,2,… — для использования годится только id товара.
Итак, стоит задача распределения массива id товаров по столбцу `order_num`.
α-Решение
Сперва нужно создать временную таблицу `by_model`, в которую разместим `product_id` товаров, отсортированных по `model`. Кроме того, нужно добавить ещё один столбец: счётчик строк. Результат будет выглядеть, например, так:
| `rowid` |
`product_id` |
| 1 |
70 |
| 2 |
10 |
| 3 |
30 |
| 4 |
20 |
Это почти тривиально. Нужно только добавить столбец `rowid` с номерами строк:
SET @n_row := 0; # Начальное значение счётчика строк
CREATE TEMPORARY TABLE `by_model`
SELECT @n_row:=@n_row+1 AS `rowid`, `product_id`
FROM `products`
ORDER BY `model` ASC;
* This source code was highlighted with Source Code Highlighter.
Аналогично создаётся ещё одна временная таблица `by_prod`: список всех `product_id`, отсортированный по возрастанию. Точно так же добавляется столбец-счётчик:
| `rowid` |
`ord` |
| 1 |
10 |
| 2 |
20 |
| 3 |
30 |
| 4 |
70 |
SET @n_ord := 0;
CREATE TEMPORARY TABLE `by_model`
SELECT @n_ord:=@n_ord+1 AS `rowid`, `product_id` AS `ord`
FROM `products`
ORDER BY `product_id` ASC;
* This source code was highlighted with Source Code Highlighter.
Попробуем-ка заJOIN'ить эти две таблицы по общему столбцу `rowid`:
CREATE TEMPORARY TABLE `products-sort`
SELECT `product_id`, `ord`
FROM `by_model` NATURAL JOIN `by_prod`;
* This source code was highlighted with Source Code Highlighter.
И получаем такой набор данных:
| `product_id` |
`order_num` |
| 70 |
10 |
| 10 |
20 |
| 30 |
30 |
| 20 |
70 |
Становится очевидно, что если товарам с id'шниками из первого столбца таблицы `products-sort` присвоить порядковый номер из второго столбца, то цель будет достигнута :)
Вот так:
UPDATE `products` NATURAL JOIN `products-sort` SET `order_num`=`ord`;
* This source code was highlighted with Source Code Highlighter.
Финальная реализация & оптимизация
Вместо создания трёх временных таблиц можно обойтись одной лишь `products-sort`, а остальные поместить как вложенный запрос внутрь конструкции WHERE. И вот как будет выглядеть всё это, собранное в кучу:
# Счётчики для нумерации строк в двух вспомогательных таблицах в подзапросе
SET @n_row := 0, @n_ord := 0;
;;;
# Создание временной таблицы, содержащей отображение нового порядкового номера `ord` на изменяемый товар `product_id`. Это отображение используется при обновлении основной таблицы.
CREATE TEMPORARY TABLE `products-sort`
SELECT `product_id`, `ord` FROM
# Вспомогательная таблица: счётчик + id товаров, отсортированные по `model`
(SELECT @n_row:=@n_row+1 AS `rowid`, `product_id`
FROM `products`
ORDER BY `model` ASC
) AS `by_model` # имя алиаса для подзапроса: используется наподобие временной таблицы
# Соединение по столбцу счётчиков: `rowid`
NATURAL JOIN
# Вспомогательная таблица: счётчик + id товаров, отсортированные по id
(SELECT @n_ord:=@n_ord+1 AS `rowid`, `product_id` AS `ord`
FROM `products`
ORDER BY `product_id` ASC
) AS `by_prod`;
;;;
# Обновление исходной таблицы с использованием данных из временной `products-sort`
UPDATE `products` NATURAL JOIN `products-sort` SET `order_num`=`ord`;
* This source code was highlighted with Source Code Highlighter.
Всего три запроса, и чистый MySQL.
О скорости исполнения в данном случае заботиться не приходится, но тем не менее она весьма приличная :)
комментарии (31)
У автора возникла проблема и он ее оригинально решил. И комментарии к этой конкретной зарисовке довольно любопытны.
SET @n_row := 0; # Начальное значение счётчика строк
CREATE TEMPORARY TABLE `by_model`
SELECT @n_row:=@n_row+1 AS `rowid`, `product_id`
FROM `products`
ORDER BY `model` ASC;
для этого существует autoincrement, а не «глобальные переменные». ибо переменная тут — костыль. я уверен что эту задачу можно решить в 1 несложный запрос, если бы мне попалась такая задача, то я бы конечно попытался и уверен что нашел бы вариант :)
а вот оптимизации всяческие — уже интересная тема, тем более что остро стоящая
посмотреть можно через show create table `by_model` и вы можете быть приятно удивлены! длины столбцов не совпадают, типы (даже для int) тоже не совпадают, потому что мускуль выбрал САМ, а выбрал — что ему по душе было. надо ручками описывать если дальше ст абличкой хочется без проблем работать, может конечно мне мускули всегда говенные попадаются и я знаю о таких подводных камнях :)
зы: переменная и правда сессионная, я имел ввиду глобальная — по отношению к запросу. то есть это не манипуляция со столбцами таблиц в запросе.
Все равно ведь айдишки продуктов попадают в колонку order_num случайным в общем то образом и ситуации, когда айди продукта из одной категории попадает в ордер_нум продукта из другой категории, будут! Значит колонку order_num для определения категории использовать нельзя и… и 0,1,2 получается тогда ничем не хуже?
Конкретно — в оба подзапроса ставится «WHERE `category_id`=69». Этот момент я опустил чтоб не загромождать суть частностями
Можно вполне даже без подзапросов обойтись:
P.S. Надо будет в следующий раз внимательнее читать.