Запросы отчетности: лучший способ объединения нескольких таблиц фактов? - PullRequest
0 голосов
/ 18 апреля 2009

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

Но у меня такое ощущение, что я не генерирую самые эффективные запросы. Для набора таблиц с несколькими миллионами записей выполнение этих запросов занимает около 10 секунд, и я бы хотел, чтобы они находились в диапазоне менее одной секунды. У меня есть ощущение, что, если бы я мог избавиться от подзапросов, результат был бы гораздо более эффективным.

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

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

concert
  id
  artist_id
  venue_id
  date

artist
  id
  name

venue
  id
  name

tag
  id
  name

artist_tag
  artist_id
  tag_id

venue_tag
  venue_id
  tag_id

Довольно просто.

Теперь, допустим, я хочу запросить базу данных для всех концертов, происходящих в течение одного месяца с сегодняшнего дня, для всех артистов с метками 'techno' и 'trombone', выступающих на концертах с 'cheap-beer' и 'great-mosh- бирка ям.

Лучший запрос, который мне удалось найти, выглядит следующим образом:

SELECT
  concert.id AS concert_id,
  concert.date AS concert_date,
  artist.id AS artist_id,
  artist.name AS artist_name,
  venue.id AS venue_id,
  venue.name AS venue_name,
FROM
  concert
INNER JOIN (
  artist ON artist.id = concert.artist_id
) INNER JOIN (
  venue ON venue.id = concert.venue_id
)
WHERE (
  artist.id IN (
    SELECT artist_id
    FROM artist_tag
    INNER JOIN tag AS a on (
      a.id = artist_tag.tag_id
      AND
      a.name = 'techno'
    ) INNER JOIN tag AS b on (
      b.id = artist_tag.tag_id
      AND
      b.name = 'trombone'
    )
  )
  AND
  venue.id IN (
    SELECT venue_id
    FROM venue_tag
    INNER JOIN tag AS a on (
      a.id = venue_tag.tag_id
      AND
      a.name = 'cheap-beer'
    ) INNER JOIN tag AS b on (
      b.id = venue_tag.tag_id
      AND
      b.name = 'great-mosh-pits'
    )
  )
  AND
  concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH)
)

Запрос работает, но мне действительно не нравится иметь эти несколько подзапросов. Если бы я мог выполнить ту же логику, просто используя логику JOIN, у меня было ощущение, что производительность резко улучшится.

В идеальном мире я бы использовал настоящий OLAP-сервер. Но мои клиенты будут развертывать MySQL, MSSQL или Postgres, и я не могу гарантировать, что будет доступен совместимый движок OLAP. Поэтому я застрял, используя обычную СУБД со звездообразной схемой.

Не зацикливайтесь на деталях этого примера (мое настоящее приложение не имеет ничего общего с музыкой, но в нем есть несколько таблиц фактов, аналогичных тем, которые я здесь показал). В этой модели таблицы 'artist_tag' и 'venue_tag' функционируют как таблицы фактов, а все остальное является измерением.

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

Итак, мой вопрос: каковы наилучшие из известных вам методов написания эффективных запросов к нескольким таблицам фактов?

Ответы [ 3 ]

2 голосов
/ 18 апреля 2009

Мой подход немного более общий, я помещаю параметры фильтра в таблицы и затем использую GROUP BY, HAVING и COUNT для фильтрации результатов. Я использовал этот базовый подход несколько раз для какого-то очень сложного «поиска», и он работает очень хорошо (для меня ухмылка ).

Я также изначально не включаю таблицы измерений Artist и Venue. Я получаю результаты в виде идентификаторов (просто требуются artist_tag и venue_tag), а затем объединяю результаты в таблицах Artist и Venue для получения этих значений измерений. (По сути, ищите идентификаторы сущностей в подзапросе, затем во внешнем запросе получите значения измерений, которые вам нужны. Хранение их отдельно должно улучшить ситуацию ...)

DECLARE @artist_filter TABLE (
  tag_id INT
)

DECLARE @venue_filter TABLE (
  tag_id INT
)

INSERT INTO @artist_filter
SELECT id FROM tag
WHERE name IN ('techno','trombone')

INSERT INTO @venue_filter
SELECT id FROM tag
WHERE name IN ('cheap-beer','great-most-pits')


SELECT
  concert.id AS concert_id,
  concert.date AS concert_date,
  artist.id AS artist_id,
  venue.id AS venue_id
FROM
  concert
INNER JOIN
  artist_tag
    ON artist_tag.artist_id = concert.artist_id
INNER JOIN
  @artist_filter AS [artist_filter]
    ON [artist_filter].tag_id = artist_tag.id
INNER JOIN
  venue_tag
    ON venue_tag.venue_id = concert.venue_id
INNER JOIN
  @venue_filter AS [venue_filter]
    ON [venue_filter].tag_id = venue_tag.id
WHERE
  concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH)
GROUP BY
  concert.id,
  concert.date,
  artist_tag.artist_id,
  venue_tag.id
HAVING
  COUNT(DISTINCT [artist_filter].id) = (SELECT COUNT(*) FROM @artist_filter)
  AND
  COUNT(DISTINCT [venue_filter].id)  = (SELECT COUNT(*) FROM @venue_filter)

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

EDIT
Примечание:

Другим вариантом будет фильтрация таблиц artist_tag и venue_tag в подзапросах / производных таблицах. Стоит ли это того, зависит ли то, насколько влиятельным является объединение на концертном столе. Мое предположение здесь состоит в том, что существует МНОГИЕ исполнители и места проведения, но после фильтрации на концертном столе (сама фильтруется по датам) количество исполнителей / мест встречи резко уменьшается.

Кроме того, часто возникает необходимость / желание иметь дело со случаем, в котором не указываются NO artist_tags и / или venue_tags. По опыту лучше заниматься этим программно. То есть используйте операторы IF и запросы, специально подходящие для этих случаев. Один SQL-запрос МОЖЕТ быть написан для его обработки, но он намного медленнее, чем программная альтернатива. Точно так же, написание похожих запросов несколько раз может показаться беспорядочным и ухудшать удобство сопровождения, но из-за усложнения необходимости сделать его единым запросом зачастую сложнее в обслуживании.

EDIT

Еще один похожий макет может быть ...
- Фильтровать концерт по исполнителю как sub_query / производная_таблица
- Отфильтруйте результаты по месту проведения как sub_query / производная_таблица
- Объедините результаты в таблицах измерений, чтобы получить имена и т. Д.

(каскадная фильтрация)

SELECT
   <blah>
FROM
  (
    SELECT
      <blah>
    FROM
      (
        SELECT
          <blah>
        FROM
          concert
        INNER JOIN
          artist_tag
        INNER JOIN
          artist_filter
        WHERE
        GROUP BY
        HAVING
      )
    INNER JOIN
      venue_tag
    INNER JOIN
      venue_filter
    GROUP BY
    HAVING
  )
INNER JOIN
  artist
INNER JOIN
  venue

При каскадной фильтрации каждая последующая фильтрация имеет набор сокращений, с которым она должна работать. Это МОЖЕТ сократить объем работы, выполняемой разделом GROUP BY - HAVING запроса. Я полагаю, что для двух уровней фильтрации это вряд ли будет драматичным.

Оригинал может быть еще более производительным, поскольку он обеспечивает дополнительную фильтрацию другим способом. В вашем примере:
- В вашем диапазоне дат может быть много артистов, но немногие соответствуют хотя бы одному критерию
- В вашем диапазоне дат может быть много мест, но мало, которые соответствуют хотя бы одному критерию
- Однако до GROUP BY все концерты исключаются, где ...
---> исполнитель (и) не соответствует ни одному из критериев
---> И / ИЛИ место проведения не соответствует ни одному из критериев

Если вы ищете по многим критериям, эта фильтрация ухудшается. Кроме того, там, где места проведения и / или артисты используют много тегов, фильтрация также ухудшается.

Так, когда я использовал бы оригинал, или когда я бы использовал Каскадную версию?
- Оригинал: несколько критериев поиска и мест / исполнителей не похожи друг на друга
- Каскадный: множество критериев поиска или мест / исполнителей, как правило, похожи

1 голос
/ 18 апреля 2009

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

Используя эту денормализацию, предложение where может проверять только это дополнительное поле tag_name в обеих таблицах (исполнитель и место проведения).

0 голосов
/ 09 марта 2015

Эта ситуация не является технически множественной таблицей фактов. У вас есть много-много отношений между местами и тегами, а также художниками и тегами.

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

Помимо сгенерированного пользователем запроса к таблице фактов, вам необходимо два статических запроса, чтобы в первую очередь предоставить пользователю параметры параметров. Один из них представляет собой список тегов, подходящих для Места, другой - для тегов, подходящих для Артиста.

Место проведения соответствующих тегов:

SELECT DISTINCT tag_id, tag.name as VenueTagName
FROM venue_tag 
INNER JOIN tag 
ON venue_tag.tag_id = tag.id

Исполнитель соответствующие теги:

SELECT DISTINCT tag_id, tag.name as ArtistTagName
FROM artist_tag 
INNER JOIN tag 
ON artist_tag.tag_id = tag.id

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

например. Когда пользователь выбирает теги, вы берете значения tag.id и предоставляете их в свой запрос (где у меня есть бит (1,2) и бит (100,200) ниже):

 SELECT
  concert.id AS concert_id,
  concert.date AS concert_date,
  artist.id AS artist_id,
  artist.name AS artist_name,
  venue.id AS venue_id,
  venue.name AS venue_name,
FROM 
concert
INNER JOIN artist 
    ON artist.id = concert.artist_id
INNER JOIN artist_tag
    ON artist.id = artist_tag.artist_id
INNER JOIN venue 
    ON venue.id = concert.venue_id
INNER JOIN venue_tag
    ON venue.id = venue_tag.venue_id
WHERE venue_tag.tag_id in ( 1,2 ) -- Assumes that the IDs 1 and 2 map to "cheap-beer" and "great-mosh-pits)
AND   artist_tag.tag_id in (100,200) -- Assumes that the IDs 100 and 200 map to "techno" and "trombone") Sounds like a wild night of drunken moshing to brass band techno!
AND concert.date BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH)
...