【问题标题】:Reporting Queries: Best Way to Join Multiple Fact Tables?报告查询:加入多个事实表的最佳方式?
【发布时间】:2010-10-20 07:14:35
【问题描述】:

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

但是,我感觉我没有生成最有效的查询。在一组有几百万条记录的表上,这些查询大约需要 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”和“长号”标签的艺术家,在音乐会上表演“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' 表用作事实表,而其他一切都是维度。

重要的是要注意,在此示例中,如果我只允许用户限制单个艺术家标签或场地标签值,则查询编写起来要简单得多。只有当我允许查询包含 AND 逻辑时,它才会变得非常棘手,需要多个不同的标签。

那么,我的问题是:您知道针对多个事实表编写高效查询的最佳技术是什么?

【问题讨论】:

  • 我觉得这里的症结确实是查询的AND性质,而不是“多个事实表”。 (尽管它们确实相互复合。)我在下面给出的答案通过在 HAVING 子句中执行查询的 AND 组件来解决这个问题,而不是多次连接到相同的事实表。
  • 时间标记为已解决/关闭/... :)

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


【解决方案1】:

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

我最初也没有加入 Artist 和 Venue 维度表。我会以 id 的形式获得结果(只需要艺术家标签和场地标签),然后将结果加入艺术家和场地表中以获得这些维度值。 (基本上,在子查询中搜索实体 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

编辑
注意:

另一个选项是过滤子查询/派生表中的艺术家标签和场地标签表。这是否值得取决于 Concert 表上的连接的影响力。我在这里的假设是有很多艺术家和场地,但是一旦在音乐会表上过滤(本身按日期过滤),艺术家/场地的数量就会急剧减少。

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

编辑

另一个类似的布局可能是...
- 按艺术家过滤音乐会为 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 之前,所有音乐会都被淘汰了...
---> 艺术家不符合任何标准
---> 和/或场地不符合任何标准

在您按许多标准搜索的地方,此过滤会降级。此外,在场地和/或艺术家共享大量标签的情况下,过滤也会降级。

那么我什么时候使用原始版本,或者我什么时候使用 Cascaded 版本?
- 原创:很少有搜索条件和场地/艺术家彼此不同
- 级联:很多搜索条件或场地/艺术家往往是相似的

【讨论】:

  • 我没有使用“tag_artist_user”表,因为它不会影响您示例中的结果
  • 糟糕。 “tag_artist_user”表是查询的先前草稿的工件。刚刚编辑了原始帖子以将其删除。
  • 我喜欢使用过滤表的方法,但不喜欢使用表变量。您没有关于这些的索引。可以在表变量上建立索引,但出于公平的原因,没有统计信息。您的解决方案也是特定于 SQL Server 的。如果使用表变量,SQL Server 将生成一个执行计划,假设表变量只有一行(由于没有统计信息)。如果表变量中的行不多,它可能会执行得很好,但是当有更多行时,性能就会下降。
  • @davos - 这本质上是在查询一个 EAV 结构,这已经很慢了,没有办法避免这种情况;这是关于代码的可维护性和灵活性。如果过滤表中有“大量”行,无论如何它都会很慢。还应该注意的是,过滤表总是要进行全表扫描。在这种情况下,相关索引是持久表上的索引,而不是过滤器。最后,BETWEEN NOW() AND (NOW() + INTERVAL 1 MONTH) 几乎不是 MS SQL Server 特定的 ;)
  • 对不起,MySQL 特定的。我同意它总是对过滤表进行全面扫描,但它可能是索引扫描而不是表扫描,如果表很大,它可以提供一些快捷方式,比如能够在准确地连接和估计行(和内存使用)。唯一索引为查询引擎提供保证。
【解决方案2】:

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

通过应用这种非规范化,where 子句只能检查两个表(艺术家和场所)中的这个额外的 tag_name 字段。

【讨论】:

  • 但是如果我去规范化,我如何允许一个艺术家或场地有多个标签?问题是,如果不完全削弱模型,我真的无法消除多对多关系。
  • 您将拥有同一位艺术家的多条记录,但标签不同。在数据仓库中,通常的做法是对数据进行非规范化,以提高查询性能。这是采用 ETL 作业(Extract-Transform-Load data)的原因之一:将规范化的关系模型转换为数据仓库特定模型(维度或星形模型)。
  • 同意,基于几个假设。这会导致数据量急剧增加,空间是否可用? (来吧,驱动器很便宜......)对于可变数据,刷新非规范化数据在 cpu 等方面的成本很高。数据是否相对静态,和/或它可以在一夜之间进行 ETL 等?如果是这样,这种非规范化(例如,平面文件格式)可能对报告非常有益。
  • 我正在考虑那样做。但我将运行大量聚合查询(计数、总和、平均、标准差等),重复记录会搞砸计算。对于计数,我可以使用 DISTINCT,但如何消除其他聚合函数的差异?
  • 这完全取决于您要编写的确切查询和确切的架构。我的经验是,“数据仓库”不涉及以一种新格式存储数据,而是几种。每个都针对不同的报告需求进行了优化。您将在那个“ETL”中安排数据以适合不同的聚合,或者实际上在那里进行聚合。不管怎样,你可能会发现你需要先解决你原来的问题......
【解决方案3】:

这种情况在技术上不是多个事实表。您在场所和标签以及艺术家和标签之间有多对多的关系。

我认为 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 传递回数据库。

例如当用户选择标签时,您获取 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)

【讨论】:

  • 请注意,WHERE venue_tag.tag_id in ( 1,2 ) 不符合 OP 的要求。这提供了具有cheap-beergreat-moshpits 的场地,但OP 希望获得具有cheap-beergreat-moshpits 的场地。这涉及检查多行(一行具有cheap-beer,一行具有great-moshpits,然后要求两行必须存在于同一地点)。此外,SQL 在参数化列表方面是出了名的差。如果 OP 需要 cheap-beer AND great-moshpits AND free-entry 怎么办?这个答案没有提供证明n 属性的通用方法。
  • @MatBailie 是的,我看你是对的,我没有考虑标签的 AND 要求。我的示例仅处理 OR 示例。我认为我关于参数处理的观点仍然有效,但我明白为什么您在第一个示例中比较 HAVING 子句中的标签计数,这确实是概括的,所以 +1。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多