MySQL

индекс
229,29

Задача на сортировку

Возможно, кому-то эта задача покажется пустяковой, но лично я потратил на неё несколько часов, израсходовав подсказки «мнение зала» и «звонок другу». Зачем я это решал? Ответ прост: мне действительно нужно было реализовать такой подход для моего небольшого сайтика Одио.ру. Если вкратце, то там публикуются записи с самых разных сайтов, стягиваемые по RSS. Сложность в том, что даты в этих записях могут полностью совпадать (даже в рамках одной ленты), при этом последовательность ID имеет смысл только в рамках одной ленты, но никак не влияет на весь поток записей. Итак, давайте перейдем к условиям задачи.

Поскольку это блог о MySQL, то приведу сразу SHOW CREATE TABLE для тестовой таблицы нашей задачи:

CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`date` datetime NOT NULL,
`content` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `date` (`date`)
) ENGINE=MyISAM;


Теперь заполняем её тестовыми данными:

INSERT INTO `test` (`id` ,`date` ,`content`)
VALUES (NULL , '2010-03-01 11:00:00', 'Test 1'),
(NULL , '2010-03-01 12:00:00', 'Test 2'),
(NULL , '2010-03-01 13:00:00', 'Test 4'),
(NULL , '2010-03-01 12:00:00', 'Test 3'),
(NULL , '2010-03-01 14:00:00', 'Test 5');


В итоге получаем следующую таблицу (SELECT * FROM `test` ORDER BY `id`):

| 1 | 2010-03-01 11:00:00 | Test 1 |
| 2 | 2010-03-01 12:00:00 | Test 2 |
| 3 | 2010-03-01 13:00:00 | Test 4 |
| 4 | 2010-03-01 12:00:00 | Test 3 |
| 5 | 2010-03-01 14:00:00 | Test 5 |


Как видно, сейчас таблица отсортирована по ID и даты идут в неправильном порядке.

Теперь, собственно, сама задача: на входе имеем одну из записей (то есть, нам известны ID и DATE), нужно получить ID соседних записей (предыдущей и следующей), при этом, если DATE совпадает, тогда у предыдущей записи будет ID меньше текущей, а у следующей — больше.

Наблюдательный читатель сразу поймет, что если сделать просто (для предыдущей записи): SELECT `id` FROM `test` WHERE `date` <= $date AND `id` != $id ORDER BY `date` DESC, `id` DESC LIMIT 1, тогда мы «зациклимся» между записями 2 и 4, потому что у них одинаковые DATE — то есть, 2 будет предыдущей для 4, а 4 будет предыдущей для 2. При этом важно, чтобы у крайних записей (1 и 5) в качестве соседних (соответственно, предыдущей для 1 и следующей для 5) ничего не возвращалось.

Чтобы упростить, давайте будем искать только предыдущую запись. Начинаем с 5 записи, имеем ID=5 и DATE='2010-03-01 14:00:00'. Нужно получить запись 3, и далее получаем уже для условий ID=3 и DATE='2010-03-01 13:00:00', и так далее…

Уточню сразу, задача имеет как минимум одно решение ;) Один запрос — одна предыдущая запись, тот же запрос с другими параметрами — следующая предыдущая запись. То есть, вариант «получить всё и ходить по этому» не подходит. Также не подходит вариант «добавить колонку ORDER_NUM и перестраивать его для всей таблицы при добавлении новой записи». Ещё больше не подходит вариант «записывать ID уже показанных записей и исключать их из выборки».

В общем, нужен «честный» запрос, который по ID и DATE текущей записи вернёт настоящий ID настоящей предыдущей записи.

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

Также, я оставляю за собой право использовать ваш запрос, если он окажется лучше того, что придумал я ;) Естественно, с вашего разрешения…

Если в условиях задачи что-то непонятно, я с удовольствием уточню в комментариях.

И, пожалуйста, не нужно писать: «Что за бред? Используйте %framework_name%, он всё сделает за вас...» — мне это не интересно, да и блог о MySQL, задача на сортировку в MySQL, так что всё делаем в рамках MySQL.

UPDATE Найдено 1 решение:

SELECT `id`
FROM `test`
WHERE `date` < $date or (`date` = $date and `id` < $id)
ORDER BY `date` DESC, `id` DESC LIMIT 1


Прислал: SabMakc

Есть и другие решения, пожалуй сложнее представленного, но, тем не менее, вы можете продолжить поиски…

UPDATE 2 Привожу «сложное» трёхэтажное решение этой задачи:

SELECT `id`, `date`, IF (`date` = $date AND id < $id, 0, 1) AS `ordr`
FROM `test`
WHERE `date` <= $date AND `id` != $id
HAVING `date` < IF (`ordr` = 1, $date, NOW())
ORDER BY `ordr`, `date` DESC, `id` DESC
LIMIT 1


Это моё решение. По сути, оно делает то же самое, что и предыдущее решение SabMakc, но немного через другие ворота…
0
1 марта 2010, 15:07
14

комментарии (25)

НЛО прилетело и опубликовало эту надпись здесь
0
blockdog #
Нужно найти предыдущую запись по дате, но если даты совпадают, тогда предыдущую запись нужно определять по ID.

Посмотрите пример, там две записи (2 и 4) с одинаковыми датами. Нужно сделать так, чтобы для 4 возвращалась 2, а для 2 возвращалась 1. Но при этом нужно, чтобы работала нормальная сортировка по дате, если записей с совпадающими датами нет.

Реально закручено всё, но на примере проще всего понять.
+2
soltpain #
жаль что в мускуле нет синтаксиса: SELECT * FROM test WHERE ID = 3 ORDER BY `Date` DESC, `ID` DESC LIMIT RELATIVE(-1,1)
иногда хочется иметь лимит относительно текущей записи… а так — два запроса туда-сюда отдельных с полной сортировкой, иначе оптимизация Limit + Where всё испортит
0
blockdog #
Опишите отдельный запрос с «полной сортировкой».
Если я правильно понимаю, то вы хотите выбрать подзапросом все записи, подходящие под условия `date` <= $date AND `id` != $id, а основным запросом отсеять те, которые не подходят по условию совпадающих дат? Наверное, так тоже можно сделать, но тут всё-таки получится два запроса (точнее запрос с подзапросом), а это не самое лучшее решение — можно одним запросом однозначно определить.
+2
NeX #
4 будет предыдущей для 2

SELECT `id` FROM `test` WHERE `date` <= $date AND `id` < $id ORDER BY `date` DESC, `id` DESC LIMIT 1
чем не подходит?
0
blockdog #
В таком запросе вы не получите предыдущую запись, если даты не совпадают, а ID предыдущей больше, чем текущей. Такое возможно по условиям задачи.
0
NeX #
Сформулируйте тогда условие задачи нормально. Судя по всему, вас мало кто может понять сейчас
0
blockdog #
Я очень долго бился над формулировкой задачи. Видимо, недостаточно.

Попробую другими словами: есть таблица с записями, нужно для каждой записи таблицы получить предыдущую запись по следующим условиям:
1) дата предыдущей записи меньше или равна текущей записи
2) если дата равна, тогда предыдущая запись определяется последовательностью ID, например, если для записей 123, 456, 789 даты равны, то для 456 предыдущей будет 123, и т.д.

В приведенной в задаче тестовой таблице правильная последовательность предыдущих ID от 5 будет следующая: 5, 3, 4, 2, 1
0
tkf #
ээ ORDER BY 'date' DESC ,'id' DESC или я что то не так понял?
0
tkf #
сорри, не увидел ниже habrahabr.ru/blogs/mysql/85945/#comment_2567913
0
blockdog #
Наверное, не так поняли.
При совпадении дат `date` DESC откидывается, и сортируется по `id` DESC.
В данном случае будет возвращаться больший из последовательности ID, отличный от текущего. То есть, для 2 мы получим 4, а для 4 получим 2 — зациклились.
0
tkf #
Такс наверно я туплю, но давайте посмотрим
Есть
2010-03-01 4
2010-03-01 2
2010-03-01 1
2010-02-28 3
в таком виде они станут после ORDER BY 'date' DESC ,'id' DESC
так что теперь надо будет сформировать условия выборки, как ниже написано WHERE `date` < $date or (`date` = $date and `id` < $id) и это вернет правильный результат.
0
blockdog #
Откуда 2010-02-28?
Все даты: 2010-03-01, отличаются только часом.
0
tkf #
это был пример
| 5 | 2010-03-01 14:00:00 | Test 5 |
| 3 | 2010-03-01 13:00:00 | Test 4 |
| 4 | 2010-03-01 12:00:00 | Test 3 |
| 2 | 2010-03-01 12:00:00 | Test 2 |
| 1 | 2010-03-01 11:00:00 | Test 1 |

Пусть будет вот так, сути это не изменит.
0
blockdog #
| 4 | 2010-03-01 12:00:00 | Test 3 |
| 2 | 2010-03-01 12:00:00 | Test 2 |


SELECT `id` FROM `test` WHERE `date` <= '2010-03-01 12:00:00' AND `id` != 4 ORDER BY `date` DESC, `id` DESC LIMIT 1
Вернет: 2

SELECT `id` FROM `test` WHERE `date` <= '2010-03-01 12:00:00' AND `id` != 2 ORDER BY `date` DESC, `id` DESC LIMIT 1
Вернет: 4

Вот и закольцевалось…

Если вы хотите `date` <= $date AND `id` < $id сделать (как в первом комментарии этой ветки), тогда вы не сможете получить предыдущую запись по дате, у которой ID больше текущего.

В моей тестовой таблице этого случая для предыдущей записи нет, но если вы попробуете таким запросом найти следующую запись: `date` >= $date AND `id` > $id, тогда 3 никогда не выпадет (по дате она после 4, но id у нее не > 4).

WHERE `date` < $date or (`date` = $date and `id` < $id) остается правильным решением, учитывающим это.
0
alexius2 #
SELECT `id` FROM `test` WHERE `date` <= $date AND `id` != $id ORDER BY `date` DESC, (`id` < $id) DESC LIMIT 1

А так не пойдет разве?
0
blockdog #
Нет. Такой запрос для 3 записи вернет 2, а должен вернуть 4.
Затем для 4 он вернет 2, а для 2 вернет 4 — зациклился…
+5
SabMakc #
SELECT `id` FROM `test` WHERE `date` < $date or (`date` = $date and `id` < $id) ORDER BY `date` DESC, `id` DESC LIMIT 1
Не оно?
0
blockdog #
Да! Молодец!
У меня намного сложнее решение, но в принципе, подход примерно такой же.
Спасибо за решение, оно действительно изящное и работает!
+2
SabMakc #
Вообще-то полностью согласно Вашей формулировке (вернее, практически):
1) дата предыдущей записи меньше текущей записи
или
2) если дата равна, тогда предыдущая запись определяется последовательностью ID
0
blockdog #
Да, тут я сам спалился, попытавшись расшифровать своё же сложное условие задачи (:
0
NeX #
не забудьте индекс добавить по date, id
0
blockdog #
CREATE TABLE `test` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`date` datetime NOT NULL,
`content` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `date` (`date`)
) ENGINE=MyISAM;
0
NeX #
KEY `date` (`date`, `id `) может лучше сработать, но надо проверить
0
blockdog #
Скорее нет, потому что в данном случае такой составной индекс замедлит условие `date` < $date, которое будет очень быстро работать при индексе KEY `date` (`date`) за счёт «отбрасывания» из индекса всего, что >= $date.

В данном конкретном примере лучше использовать два индекса. Но, спасибо вам за то, что акцентировали на этом внимание — это действительно важно!

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