Агрегирование строк SQL с приоритетом - PullRequest
0 голосов
/ 06 декабря 2009

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

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

У меня есть таблица sources с информацией о каждом источнике и полями location_id и location_precedence. Затем у меня есть таблица items, содержащая каждый элемент, его unique_id, source_id и content. Предметы с одинаковым unique_id и источником 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');

Ожидаемое содержимое после запроса:

  • Рассказ о Субъекте
  • Тече Боже
  • Фу, Бар и Кукс в суде
  • Протесты против Фубара вызывают беспокойство
  • Microsoft Bleh встречает теплый прием
  • Вы когда-нибудь думали сделать это в VB?
  • У знаменитости появился новый друг
  • Microsoft выпускает Bleh по всему миру
  • Технический обзор
  • Состояния "старые новости" говорят правительство
  • Иранский доктор "отравлен"
  • Поклонники Apple в восторге от 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 и Quux в судебном сражении» (источник «BBC Technology News». Просмотр результатов внутреннего запрос, я получаю:

unique_id: 'ghi'
source_id: 2
prio: 15

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

Ответы [ 3 ]

5 голосов
/ 06 декабря 2009

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 написал хорошую статью .

РЕДАКТИРОВАТЬ: Одним из способов разрыва связей с несколькими источниками с одинаковым приоритетом является предложение 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 также есть статья о выборе записей, содержащих максимум по группам (разрешение связей) :)

1 голос
/ 06 декабря 2009

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

select max(location_priority) from table where ...
0 голосов
/ 06 декабря 2009

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

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 не является ни совокупным, ни сгруппированным. В результате, какое значение вы получите, является неопределенным.

...