如何在这种情况下避免选择类似N + 1的问题?

时间:2011-09-14 03:03:49

标签: sql greatest-n-per-group

场景:我需要显示最近20个报告值的平均值。我需要为所有用户执行此操作。我使用的是Sql Server 2005 Express。这是我需要支持的最低版本的数据库服务器。

我现在这样做的方法是:1个查询来获取所有用户。每个用户1次查询以获取最近20个报告的值。虽然由于商业原因我实际上无法在sql中执行平均值,但我们暂时假设我可以。

有了这个假设,在我的脑海中,sql按日期排序,每个用户限制20行,最后按用户ID排序。不幸的是,在sql中似乎没有任何方法可以做到这一点。

有没有办法避免N + 1查询?

EDIT1:

Ericb的回答完成了工作。然而,我会等待一段时间才能将其标记为答案,原因有两个。

  1. 我想知道这种方法是否有任何性能损失。报告表将包含每个用户数万行。虽然我只需要平均最新的20个。
  2. 我很想修改问题(即删除假设)并反映我的业务需求。我希望甚至可以在SQL中解决这个问题。
  3. 同样的问题,但删除了假设:

    平均需要在最近的20份连续报告中完成。意思是,假设最新的20行(按顺序排列)包含15行(20到6),时间是下午2:25到2:40 PM。并且行5到1包含的时间是下午2:43到下午2:48 ... 最近的连续数据集是第5行到第1行。因此,仅需要对这5行进行平均< / EM> 即可。它不像数据会分批出现,所以数字15和5可以很容易地分为10和10或3和5和12甚至全部20连续(为简单起见我假设最新的20将全部是连续的。

    你们觉得怎么样?可以在sql中完成,还是在c#中最好处理?

    编辑2: 我在考虑它。在c#中,我将从最近的日期开始。减去1分钟。并检查下一个最近的日期是否与此值匹配。如果是,请将其添加到列表中。看看这些步骤,我无法想象如何在sql中复制这样的东西。事实上,我仍然不确定ericb答案的c#等价物是什么。这让我想知道,在sql中怎么想?

4 个答案:

答案 0 :(得分:4)

希望我正在解释这一点。我假设一个非常基本的表设置:

CREATE TABLE Reports
(
    UserId INT,
    Report INT,
    CreatedOn DATETIME  
)

CREATE TABLE Users
(
    UserId INT
)


SELECT  x.UserId, AVG(x.Report) as Report_Avg
FROM
        (
        SELECT  R.Report, U.UserId, ROW_NUMBER() OVER (PARTITION BY U.UserId ORDER BY R.CreatedOn DESC) as RowNum
        FROM    Reports R
                INNER JOIN Users U
                ON R.UserId = U.UserId
        ) x
WHERE   x.RowNum <= 20
GROUP BY x.UserId

我的代码确实使用了PARTITION BYROW_NUMBER语法,该语法应该是ANSI SQL的一部分。

答案 1 :(得分:2)

根据您的更改,您可以尝试这样的事情......

NB:这是基于所有数据都是分钟的假设,并且不会重复时间戳。如果这个假设是错误的,我建议发布您的实际数据结构并描述可输入数据的确切行为。

WITH
  mostRecentData AS
(
  SELECT
    userID,
    MAX(TimeStamp) AS TimeStamp
  FROM
    yourData
  GROUP BY
    userID
)
,
  ordered_data AS
(
  SELECT
    [reportData].*,
    DATEDIFF(minute, [reportData].TimeStamp, [mostRecentData].TimeStamp) AS offset,
    ROW_NUMBER() OVER (PARTITION BY [reportData].UserID ORDER BY [reportData].TimeStamp DESC) AS sequenceID
  FROM
    yourData                AS [reportData]
  INNER JOIN
    [mostRecentData]
      ON [reportData].userID = [reportData].UserID
)

SELECT
  UserID,
  AVG(someField)
FROM
  orderedData
WHERE
  sequenceID <= 20             -- At most the 20 most recent values
  AND sequenceID - offset = 1  -- Only Consecutive entries from the latest entry
GROUP BY
  UserID

假设您有适当的索引,sequenceID <= 20将快速解决,确保您不会为每个用户解析每条记录。

然而,sequenceID - offset不会使用索引,因此将对这20条记录中的每条记录进行处理。但这真的不是一个很大的开销。

示例数据显示sequenceID - offset = 1确实获得了最新的连续数据集......

TimeStamp  |  Row_Number()  |  Offset  |  Row_Number() - Offset

  12:24            1             0                1
  12:23            2             1                1
  12:22            3             2                1
  12:20            4             4                0
  12:19            5             5                0
  12:17            6             7               -1

答案 2 :(得分:0)

可能是一个坏主意,但也许这会让你走上正轨?

select id
from users u
left outer join 
  (
    select value
    from reported_values
    where user_id in (1,2,3)
    order by created_at desc limit 20
  ) as v
  on u.id = v.user_id
where id in (1,2,3)

答案 3 :(得分:0)

首先,如果您知道报告值的比率,或者至少是报告值的最小比率,您可以找到最早的日期并按日期过滤。只要您在日期列上建立索引,这应该通过减少查询的行数来提高性能。

接下来,您可以按用户名分组并使用sum()函数聚合每个用户。这样可以节省N-1个查询并避免使用第一个查询,这意味着:1个查询。

示例:

select username, sum(value), count(value) as numvals from table where date > [calculated earliest date/time] group by username

有了计数,你可以做两件事。

  1. 如果您只需要一个“足够接近”的值,您可以简单地将总和(值)除以计数(值)并得到一个接近的平均值。
  2. 如果您能负担几次迭代,可以添加'having numvals = 20'子句并更改日期,直到获得所有用户
    • 这比限制方法有点模糊,但避免了排序。如果你很清楚要过滤哪个日期才能获得20个值,这是有道理的。
    • 如果我必须在排序和计算保存内存,I / O周期和CPU的平均值之间做出选择,我会在周日每次和每两次选择平均值。
  3. 或者,您可以删除两个聚合和group by子句,首先按用户名排序,然后按日期排序,只需选择用户名和值。然后在进行平均计算时,在DB外部进行计数(最近20次)过滤。

    select username, value from table order by username, date
    

    我的建议的成本是,除非您的用户以相同的速率获得值,否则限制不起作用,因为它会限制所有用户。但是,如果查询的数量是主要问题,我认为这些问题可以解决这个问题。

    警告:我不是数据库人,所以上面的语法可能很恐怖,我的想法可能是由于脑损伤。但是,我建议确定基准测试。