报告查询:加入多个事实表的最佳方式?

时间:2009-04-18 15:35:22

标签: sql performance olap fact-table dimensional-modeling

我正在开发一个报告系统,允许用户任意查询一组事实表,约束每个事实表的多个维度表。我编写了一个查询构建器类,它根据约束参数自动组装所有正确的连接和子查询,并且一切都按设计工作。

但是,我觉得我没有生成最有效的查询。在一组具有几百万条记录的表中,这些查询运行大约需要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引擎可用。所以我坚持使用普通的RDBMS和星型模式。

不要太在意这个例子的细节(我的真实应用程序与音乐无关,但它有多个事实表与我在这里显示的类似关系)。在这个模型中,'artist_tag'和'venue_tag'表用作事实表,其他一切都是维度。

在这个例子中,重要的是要注意,如果我只允许用户限制单个artist_tag或venue_tag值,则查询要简单得多。当我允许查询包含AND逻辑时,它只会变得非常棘手,需要多个不同的标记。

所以,我的问题是:对于编写针对多个事实表的有效查询,您知道哪些最好的技术?

3 个答案:

答案 0 :(得分:2)

我的方法更通用,将过滤器参数放在表中,然后使用GROUP BY,HAVING和COUNT来过滤结果。我已经多次使用这种基本方法进行一些非常复杂的“搜索”,并且效果非常好(对我来说, grin )。

我最初也没有加入Artist和Venue维度表。我将结果作为id(只需要artist_tag和venue_tag)然后将结果加入到艺术家和场地表中以获得这些维度值。 (基本上,在子查询中搜索实体id,然后在外部查询中获取所需的维度值。将它们分开可以改善事物......)

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)

(我正在使用上网本并为此付出痛苦,因此我将省略外部查询,从艺术家和场地表中获取艺术家和地点名称​​ grin

修改
注意:

另一种选择是过滤子查询/派生表中的artist_tag和venue_tag表。这是否值得,取决于音乐会桌上的加入有多大影响力。我的假设是有很多艺术家和场地,但一旦在音乐会桌子上过滤(本身按日期过滤),艺术家/场地的数量就会大幅减少。

此外,经常需要/希望处理没有指定artist_tags和/或venue_tags的情况。从经验来看,最好以编程方式处理这个问题。也就是说,使用特别适合这些情况的IF语句和查询。可以编写单个SQL查询来处理它,但是比编程方案慢得多。同样地,多次编写类似的查询可能看起来很混乱并且降低了可维护性,但是复杂性的增加需要将其作为单个查询通常更难维护。

修改

另一个类似的布局可能是......
- 按艺术家过滤音乐会as sub_query / derived_table
- 按场地过滤结果为sub_query / derived_table
- 在维度表上加入结果以获取名称等

(级联过滤)

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之前,所有的音乐会都被淘汰了...... ---&GT;艺术家符合标准的要求
---&GT;和/或场地符合标准

按照许多条件搜索的地方,此过滤会降级。此外,场地和/或艺术家共享大量标签,过滤也会降低。

那么我什么时候才能使用原版,或何时使用Cascaded版本? - 原文:很少有搜索标准和场地/艺术家彼此不相似 - 级联:很多搜索标准或场地/艺术家往往相似

答案 1 :(得分:1)

使模型非规范化。在场地和艺术家表中包含标签名称。这样,您就可以避免多对多的关系,并且您拥有一个简单的星型模式。

通过应用此非规范化,where子句只能在两个表(艺术家和场地)中检查此附加tag_name字段。

答案 2 :(得分:0)

这种情况在技术上不是多个事实表。场馆和场馆之间有很多很多关系。标签以及艺术家&amp;标签。

我认为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

这两个查询驱动一些下拉菜单或其他参数选择控件。在报告系统中,您应该尝试避免传递字符串变量。在您的应用程序中,您将变量的字符串名称提供给用户,但将整数ID传递回数据库。

e.g。当用户选择代码时,您会获取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)