我正在开发一个报告系统,允许用户任意查询一组事实表,约束每个事实表的多个维度表。我编写了一个查询构建器类,它根据约束参数自动组装所有正确的连接和子查询,并且一切都按设计工作。
但是,我觉得我没有生成最有效的查询。在一组具有几百万条记录的表中,这些查询运行大约需要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逻辑时,它只会变得非常棘手,需要多个不同的标记。
所以,我的问题是:对于编写针对多个事实表的有效查询,您知道哪些最好的技术?
答案 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)