Агрегирование строк SQL с приоритетом

0

У меня есть таблица, полная статей из разных источников. Некоторые источники могут иметь одно и то же место (в моем примере разные новостные каналы BBC будут разными источниками, но все они происходят из Би-би-си). Каждый элемент имеет уникальный идентификатор, который может использоваться для идентификации его среди других из того же места. Это означает, что элементы, относящиеся к одной и той же новостной ленте на сайте, но опубликованные в разных каналах, будут иметь один и тот же "уникальный идентификатор", но это не обязательно глобально уникально.

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

У меня есть таблица sources с информацией об каждом источнике и location_id и location_precedence. Затем у меня есть таблица items, содержащая каждый элемент, unique_id, source_id и content. Элементы с теми же unique_id и source location_id должны появляться не более одного раза с наивысшим уровнем выигрыша location_precedence.

Я бы подумал, что что-то вроде:

SELECT `sources`.`name` AS `source`,
       `items`.`content`,
       `items`.`published`
FROM `items` INNER JOIN `sources`
  ON `items`.`source_id` = `sources`.`id` AND `sources`.`active` = 1
GROUP BY `items`.`unique_id`, `sources`.`location_id`
ORDER BY `sources`.`location_priority` DESC

сделал бы трюк, но это, похоже, игнорирует поле приоритета местоположения. Что я пропустил?


Пример данных:

CREATE TABLE IF NOT EXISTS `sources` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `location_id` int(10) unsigned NOT NULL,
  `location_priority` int(11) NOT NULL,
  `active` tinyint(1) unsigned NOT NULL default '1',
  `name` varchar(150) NOT NULL,
  `url` text NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `active` (`active`)
);

INSERT INTO `sources` (`id`, `location_id`, `location_priority`, `active`, `name`, `url`) VALUES
(1, 1, 25, 1, 'BBC News Front Page', 'http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml'),
(2, 1, 10, 1, 'BBC News England', 'http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/england/rss.xml'),
(3, 1, 15, 1, 'BBC Technology News', 'http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/technology/rss.xml'),
(4, 2, 0, 1, 'Slashdot', 'http://rss.slashdot.org/Slashdot/slashdot'),
(5, 3, 0, 1, 'The Daily WTF', 'http://syndication.thedailywtf.com/TheDailyWtf');

CREATE TABLE IF NOT EXISTS `items` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `source_id` int(10) unsigned NOT NULL,
  `published` datetime NOT NULL,
  `content` text NOT NULL,
  `unique_id` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`),
  UNIQUE KEY `unique_id` (`unique_id`,`source_id`),
  KEY `published` (`published`),
  KEY `source_id` (`source_id`)
);

INSERT INTO `items` (`id`, `source_id`, `published`, `content`, `unique_id`) VALUES
(1,  1, '2009-12-01 16:25:53', 'Story about Subject One',                     'abc'),
(2,  2, '2009-12-01 16:21:31', 'Subject One in story',                        'abc'),
(3,  3, '2009-12-01 16:17:20', 'Techy goodness',                              'def'),
(4,  2, '2009-12-01 16:05:57', 'Further updates on Foo case',                 'ghi'),
(5,  3, '2009-12-01 15:53:39', 'Foo, Bar and Quux in court battle',           'ghi'),
(6,  2, '2009-12-01 15:52:02', 'Anti-Fubar protests cause disquiet',          'mno'),
(7,  4, '2009-12-01 15:39:00', 'Microsoft Bleh meets lukewarm reception',     'pqr'),
(8,  5, '2009-12-01 15:13:45', 'Ever thought about doing it in VB?',          'pqr'),
(9,  1, '2009-12-01 15:13:15', 'Celebrity has 'new friend'',        'pqr'),
(10, 1, '2009-12-01 15:09:57', 'Microsoft launches Bleh worldwide',           'stu'),
(11, 2, '2009-12-01 14:57:22', 'Microsoft launches Bleh in UK',               'stu'),
(12, 3, '2009-12-01 14:57:22', 'Microsoft launches Bleh',                     'stu'),
(13, 3, '2009-12-01 14:42:15', 'Tech round-up',                               'vwx'),
(14, 2, '2009-12-01 14:36:26', 'Estates 'old news' say government', 'yza'),
(15, 1, '2009-12-01 14:15:21', 'Iranian doctor 'was poisoned'',     'bcd'),
(16, 4, '2009-12-01 14:14:02', 'Apple fans overjoyed by iBlah',               'axf');

Ожидаемый контент после запроса:

  • Рассказ о предмете 1
  • Техническая доброта
  • Foo, Bar и Quux в суде.
  • Протесты против Фубара вызывают беспокойство
  • Microsoft Bleh встречает теплый прием
  • Когда-либо думал об этом в VB?
  • Знаменитость имеет "нового друга"
  • Microsoft запускает Bleh во всем мире
  • Технический обзор
  • Старые новости Estates 'say Правительство
  • Иранский врач был отравлен
  • Яблочные поклонники радуются iBlah

Я пробовал вариант решения Andomar с некоторым успехом:

SELECT      s.`name` AS `source`,
            i.`content`,
            i.`published`
FROM        `items` i
INNER JOIN  `sources` s
ON          i.`source_id` = s.`id`
AND         s.`active` = 1
INNER JOIN (
  SELECT `unique_id`, `source_id`, MAX(`location_priority`) AS `prio` 
  FROM `items` i
  INNER JOIN `sources` s ON s.`id` = i.`source_id` AND s.`active` = 1
  GROUP BY `location_id`, `unique_id`
) `filter`
ON          i.`unique_id` = `filter`.`unique_id`
AND         s.`location_priority` = `filter`.`prio`
ORDER BY    i.`published` DESC
LIMIT 50

С AND s.location_priority = filter.prio все работает так, как я хочу. Поскольку элемент может поступать из нескольких источников с одинаковым приоритетом, элементы могут быть повторены. В этом случае дополнительный GROUP BY i.unique_id для внешнего запроса выполняет задание, и я полагаю, что не имеет значения, какой источник "выигрывает", если приоритеты равны.

Я попробовал вместо AND i.source_id = filter.source_id, который почти работает (т.е. удаляет лишний GROUP BY), но не дает результатов от правильных источников. В приведенном выше примере он дает мне "Дальнейшие обновления в случае Foo" (источник "BBC News England" ), а не "Foo, Bar and Quux in court battle" (источник "BBC Technology News". Глядя на результаты внутреннего запроса, я получаю:

unique_id: 'ghi'
source_id: 2
prio: 15

Обратите внимание, что исходный идентификатор неверен (ожидается: 3).

  • 0
    Вы можете ORDER BY, не включая 'location_priority' в столбцы GROUP BY?
  • 0
    @Yonatan Karni: В MySQL вы можете. Он ведет себя как агрегатная функция any() :)
Показать ещё 4 комментария
Теги:
group-by
aggregation

3 ответа

5
Лучший ответ

Order by просто заказывает строки, он не выбирает среди них.

Один из способов отфильтровать строки с нижним location_priority - использовать inner join как фильтр:

SELECT     s.name, i.content, i.published
FROM       items i 
INNER JOIN sources s
ON         i.source_id = s.id
AND        s.active = 1
INNER JOIN (
    SELECT unique_id, max(location_priority) as prio
    FROM items i
    INNER JOIN sources s ON s.id = i.source_id AND s.active = 1
    GROUP BY unique_id) filter
ON         i.unique_id = filter.unique_id
AND        s.location_priority = filter.prio;

Альтернативой является предложение where ... in <subquery>, например:

SELECT     s.name, i.content, i.published
FROM       items i 
INNER JOIN sources s
ON         i.source_id = s.id
AND        s.active = 1
WHERE      (i.unique_id, s.location_priority) IN (
    SELECT unique_id, max(location_priority)
    FROM items i
    INNER JOIN sources s ON s.id = i.source_id AND s.active = 1
    GROUP BY unique_id
);

Эта проблема также известна как "Выбор записей, имеющих максимальный уровень для всей группы". Quassnoi написал хорошую статью на нем.

EDIT: одним из способов разрыва связей с несколькими источниками с одинаковым приоритетом является предложение WHERE с подзапросом. Этот пример разбивает связи на i.id DESC:

SELECT     s.name, i.unique_id, i.content, i.published
FROM       (
           SELECT unique_id, min(location_priority) as prio
           FROM items i
           INNER JOIN sources s ON s.id = i.source_id AND s.active = 1
           GROUP BY unique_id
           ) filter
JOIN       items i
JOIN       sources s
ON         s.id = i.source_id 
           AND s.active = 1
WHERE      i.id =
           (
           SELECT   i.id
           FROM     items i
           JOIN     sources s 
           ON       s.id = i.source_id 
                    AND s.active = 1
           WHERE    i.unique_id = filter.unique_id
           AND      s.location_priority = filter.prio
           ORDER BY i.id DESC
           LIMIT 1
           )

В Quassnoi также есть статья о выбор записей, содержащих групповой максимум (разрешение связей):)

  • 0
    Спасибо! Статья (и умение описать проблему) очень полезна.
  • 0
    Смотрите также: dev.mysql.com/doc/refman/5.1/en/…
Показать ещё 1 комментарий
1

сделать самостоятельное соединение с производной таблицей, например

select max(location_priority) from table where ...
0

Что я пропустил?

ORDER BY происходит после того, как GROUP BY уже уменьшил каждую группу до одной строки. Пол дает одно разрешение.

Что касается проблемы с запросом:

SELECT `unique_id`, `source_id`, MAX(`location_priority`) AS `prio` 
FROM `items` i
INNER JOIN `sources` s ON s.`id` = i.`source_id` AND s.`active` = 1
GROUP BY `location_id`, `unique_id`

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

  • 0
    Это не сработает: вы не можете использовать неагрегированный столбец в предложении HAVING. Даже если бы вы могли, это скрыло бы все истории, которые имеют неактивный источник с высоким приоритетом.
  • 0
    @ Andormar: в MySQL вы можете. Объединение гарантирует, что неактивные источники с самым высоким приоритетом никогда не рассматриваются. Реальная проблема заключается в том, что HAVING, по-видимому, фильтрует после того, как GROUP BY уменьшил строки.
Показать ещё 7 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню