【问题标题】:Need to "merge" blocks of records in query into one, depending on other query columns需要根据其他查询列将查询中的记录块“合并”为一个
【发布时间】:2011-02-16 15:34:34
【问题描述】:

我有以下数据库方案。

CREATE TABLE Twix.dbo.Sensors (Id int PRIMARY KEY NOT NULL IDENTITY(1,1), Location nvarchar(260) NOT NULL);

CREATE TABLE Twix.dbo.Visitors
(Id int PRIMARY KEY NOT NULL IDENTITY(1,1),
Name nvarchar(260) NOT NULL);

CREATE TABLE Twix.dbo.Visits(Id int PRIMARY KEY NOT NULL IDENTITY(1,1),
SensorId int CONSTRAINT FK__Visits__Sensor__239E FOREIGN KEY REFERENCES Sensors(Id),
VisitorId int CONSTRAINT FK__Visits__Visitors__4DCF FOREIGN KEY REFERENCES Visitors(Id),
InTime datetime NOT NULL,
OutTime datetime NOT NULL);

访问表中的 InTime-OutTime 时段可以相交。 IE。部分真实访问。例如,

SELECT VisitorId, InTime, OutTime FROM Visits ORDER BY VisitorId, InTime

1   2011-02-09 15:26:59.173 2011-02-09 15:29:22.097
1   2011-02-09 15:28:40.530 2011-02-09 15:29:52.737
1   2011-02-09 15:30:10.577 2011-02-09 16:17:58.967
1   2011-02-09 19:09:23.057 2011-02-09 19:10:57.493
1   2011-02-09 19:12:16.087 2011-02-09 19:13:27.493
1   2011-02-14 15:08:46.333 2011-02-14 15:26:42.433
2   2011-02-09 17:55:42.390 2011-02-09 18:52:03.780
2   2011-02-09 18:10:56.727 2011-02-09 18:11:57.493
2   2011-02-09 21:47:15.650 2011-02-09 21:48:38.783
2   2011-02-09 21:50:18.337 2011-02-09 21:55:26.777
3   2011-02-09 17:12:58.103 2011-02-09 19:51:59.697
3   2011-02-09 22:27:52.073 2011-02-09 23:03:24.753
3   2011-02-09 23:02:51.177 2011-02-10 09:51:14.890
3   2011-02-14 15:27:42.270 2011-02-14 15:42:31.107
3   2011-02-14 15:43:37.320 2011-02-14 18:45:26.163
4   2011-02-09 21:07:51.030 2011-02-09 21:51:02.880
4   2011-02-09 22:42:52.660 2011-02-09 23:21:13.830
4   2011-02-09 23:23:08.563 2011-02-09 23:35:12.847
4   2011-02-09 23:36:05.120 2011-02-09 23:59:02.813
4   2011-02-10 05:58:44.103 2011-02-10 05:59:55.867
4   2011-02-12 08:29:36.620 2011-02-12 09:51:18.510
4   2011-02-12 13:13:42.650 2011-02-12 14:06:01.473
5   2011-02-10 06:48:52.717 2011-02-10 07:37:04.870
5   2011-02-10 06:50:31.067 2011-02-10 06:52:20.877
5   2011-02-10 06:52:36.273 2011-02-10 06:53:36.523
5   2011-02-10 06:59:11.790 2011-02-10 07:00:34.867
5   2011-02-10 08:36:39.563 2011-02-10 08:46:14.760
5   2011-02-10 12:47:05.567 2011-02-10 12:48:05.860
5   2011-02-10 12:49:19.590 2011-02-10 13:09:27.880
5   2011-02-10 12:49:25.733 2011-02-10 12:59:59.883
5   2011-02-10 12:55:23.460 2011-02-10 12:56:23.507

我需要“完成访问”,即合并具体访问者的所有访问,如果访问相交,或者它们之间的时间差小于 10 分钟(即“访问 2 的进入时间”-“访问 1 的外出时间”

在 C# 中它看起来像:

private IEnumerable Merge(IEnumerable visits, uint holeInterval)

    {
        var vlist = new LinkedList<Visit>(visits.OrderBy(o => o.InTime));
        var result = new List<Visit>();
        while (vlist.Count > 1)
        {
            Visit a = vlist.First.Value; vlist.RemoveFirst();
            Visit b = vlist.First.Value; vlist.RemoveFirst();
            var r = Visit.Merge(a, b, holeInterval); // Merges two visits
            if (r != null) { vlist.AddFirst(r); }
            else { result.Add(a); vlist.AddFirst(b); }
        }
        result.Add(vlist.First.Value);
        return result;
    }
public IEnumerable<Visit> ListCompleteVisits()
    {
        var result = new List<Visit>();
        var queryResult = from visits in this.repository.ListVisits()
                          group visits by visits.Visitor.Id into vgroup
                          select Merge(vgroup, this.holeInterval);
        foreach (var v in queryResult)
        {
            result.AddRange(v);
        }
        return result;
    }

我试过了,结果如下:

DECLARE @holeInterval int SET @holeInterval = 10

SELECT t.RowNumber, t.VisitorId, t.InTime, t.OutTime, t.BInMinusAOut,
    (SELECT MIN(InTime) FROM Visits AS D
     WHERE VisitorId NOT IN (select VisitorId from Visits group by VisitorId having COUNT(*) = 1)
     AND D.InTime <= t.InTime and D.VisitorId = t.VisitorId 
     AND t.RowNumber >  /*here should be max rownumber greater than holeinterval*/
    ) AS MinInTime
FROM
    (SELECT 
     ROW_NUMBER() OVER(ORDER BY VisitorId, InTime ASC) AS RowNumber, 
     VisitorId, InTime, OutTime,       
     DATEDIFF(MI,InTime,
            (SELECT MIN(InTime) FROM Visits AS B
             WHERE B.InTime > A.InTime and A.VisitorId = B. VisitorId
            )) AS BInMinusAIn,
     DATEDIFF(MI,OutTime,
            (SELECT MIN(InTime) FROM Visits AS B
             WHERE B.InTime > A.InTime and A.VisitorId = B. VisitorId
            ))) AS BInMinusAOut
FROM Visits AS A
WHERE VisitorId NOT IN (select VisitorId from Visits group by VisitorId having COUNT(*) = 1)
) t
/*WHERE t.BInMinusAOut > @holeInterval OR t.BInMinusAOut IS NULL*/
ORDER BY VisitorId, InTime

1   1   2011-02-09 15:26:59.173 2011-02-09 15:28:22.097 0
2   1   2011-02-09 15:28:40.530 2011-02-09 15:29:52.737 1
3   1   2011-02-09 15:30:10.577 2011-02-09 16:17:58.967 9
4   1   2011-02-09 16:26:44.810 2011-02-09 16:51:46.423 20
5   1   2011-02-09 17:11:57.633 2011-02-09 17:13:20.680 2
6   1   2011-02-09 17:15:35.727 2011-02-09 17:18:48.493 -2
7   1   2011-02-09 17:16:12.230 2011-02-09 17:42:47.867 3
8   1   2011-02-09 17:45:43.793 2011-02-09 17:52:10.860 3
9   1   2011-02-09 17:55:31.127 2011-02-09 20:13:22.743 -109
10  1   2011-02-09 18:24:00.427 2011-02-09 18:32:12.033 2
11  1   2011-02-09 18:34:15.877 2011-02-09 18:37:19.770 2
12  1   2011-02-09 18:39:46.440 2011-02-09 18:48:16.800 2
13  1   2011-02-09 18:50:59.270 2011-02-09 20:03:47.550 -54
14  1   2011-02-09 19:09:23.057 2011-02-09 19:10:57.493 2
15  1   2011-02-09 19:12:16.087 2011-02-09 19:13:27.493 48

现在我需要将 1st-4th “合并”到 2011-02-09 15:26:59.173 - 2011-02-09 16:51:46.423 5th-15h 进入 2011-02-09 17:11:57.633 - 2011-02-09 20:13:22.743 这意味着,我必须在行中取最小 InTime,它介于当前和最后一个之间,其中 MinInTime > @holeInterval,以及这些范围的最大 OutTime。

I.e. result:

1   2011-02-09 15:26:59.173 2011-02-09 16:51:46.423
1   2011-02-09 17:11:57.633 2011-02-09 20:13:22.743

谢谢。

【问题讨论】:

  • 了解您正在使用的数据库确实非常有帮助。我知道如何在 MySQL 和 Oracle 中执行此操作,但您使用的技术完全不相关。
  • 但你知道标签是 tsql 这意味着它是 MS SQL Server
  • @baalazamon:很好,在这种情况下,我能想到的唯一方法就是使用游标。由于我从未使用过 MS SQL Server,所以我会让其他人写出来。

标签: c# sql sql-server tsql


【解决方案1】:

不要试图在一个大的选择语句中做所有事情。 编写一个使用Select INTO #temp 语法创建临时表的存储过程。然后修改/附加临时表。最后,从临时表中选择作为输出。表现会很好,你可以继续前进。

【讨论】:

  • +1 因为,虽然这可能不是最优雅的解决方案,但它可以完成工作,现在有一个好的解决方案可能比有一天完美的解决方案更好。当然,Stackd Overflow 上的其他聪明人已经提供了出色的解决方案!
【解决方案2】:

查询是使用公用表表达式编写的,以便更清楚地了解正在发生的事情,但您只需 UNION ALL 2 个查询即可获得完全相同的结果。

查询中发生了什么:

重叠访问

通过将 InTime 和 OutTime 扩展 10 分钟来连接表。这将从数据集中选择所有重叠的访问。然后CASE 语句用于从 2 个重叠时间跨度中选择最小值作为 InTime,并选择最大值作为 OutTime。由于我们仍然通过这样做得到多个结果,因此使用 min/max 的 groupby 来过滤掉完整结果集的最小和最大时间戳。

NonOverlappingVisits

与重叠访问类似,时间跨度在任一侧扩展 10 分钟以查找重叠部分并与 WHERE NOT EXISTS 匹配。

Declare @holeinterval int

SET @holeinterval = 10
;WITH OverlappingVisits (VisitorId, InTime, OutTime)
AS (select v1.VisitorId
     , InTime =  MIN(CASE WHEN v1.InTime < v2.InTime THEN v1.InTime ELSE v2.InTime END) 
     , OutTime = MAX(CASE WHEN v1.OutTime < v2.OutTime THEN v2.OutTime ELSE v1.OutTime END) 
FROM Visits v1
  INNER JOIN Visits v2
  ON v1.VisitorId = v2.VisitorId 
  AND (v1.InTime BETWEEN dateadd(minute, -@holeinterval, v2.InTime) AND dateadd(minute, @holeinterval, v2.OutTime)
  OR v1.OutTime BETWEEN dateadd(minute, @holeinterval, v2.InTime) AND dateadd(minute, @holeinterval, v2.OutTime))
  AND v1.Id <> v2.Id
GROUP BY v1.VisitorId),
NonOverlappingVisits (VisitorId, InTime, OutTime)
AS (
SELECT v1.VisitorId
     ,  v1.InTime 
     ,  v1.OutTime 
FROM Visits v1
WHERE NOT EXISTS(SELECT *
  FROM Visits v2
  WHERE v1.VisitorId = v2.VisitorId 
  AND (v1.InTime BETWEEN dateadd(minute, -@holeinterval, v2.InTime) AND dateadd(minute, @holeinterval, v2.OutTime)
  OR v1.OutTime BETWEEN dateadd(minute, @holeinterval, v2.InTime) AND dateadd(minute, @holeinterval, v2.OutTime))
  AND v1.Id <> v2.Id))
SELECT *
FROM OverlappingVisits
UNION ALL
SELECT *
FROM NonOverlappingVisits
ORDER BY VisitorId, InTime, OutTime

【讨论】:

    【解决方案3】:
    ;WITH
    Visits_tuned AS (
      /* adding some helper columns */
      SELECT
        VisitorId,
        InTime,
        OutTime,
        OutTimeDelayed = DATEADD(minute, 10, OutTime),
        rownum = ROW_NUMBER() OVER (PARTITION BY VisitorId
                                    ORDER BY InTime, OutTime),
        VisitId = ROW_NUMBER() OVER (ORDER BY VisitorId, InTime, OutTime)
      FROM Visits
    ),
    
    Visits_starts AS (
      /* spotting the starting points of the 'merged' visits */
      SELECT DISTINCT
        v1.VisitId,
        IsStart = 1 - CASE WHEN v2.VisitId IS NULL THEN 0 ELSE 1 END
      FROM Visits_tuned v1
        LEFT JOIN Visits_tuned v2
          ON v1.InTime BETWEEN v2.InTime AND v2.OutTimeDelayed
            AND v1.VisitorId = v2.VisitorId AND v1.rownum <> v2.rownum
    ),
    
    Visits_rec AS (
      /* basically, selecting the original data, but with
         InTime values replaced by the starting InTimes */
      SELECT
        VisitId,
        VisitorId,
        InTime,
        OutTime
      FROM Visits_tuned
      WHERE VisitId = 1
    
      UNION ALL
    
      SELECT
        v.VisitId,
        v.VisitorId,
        IntTime = CASE
          WHEN v.VisitorId = r.VisitorId AND
               s.IsStart = 0 AND r.InTime < v.InTime
            THEN r.InTime
          ELSE v.InTime
        END,
        v.OutTime
      FROM Visits_tuned v
        INNER JOIN Visits_rec r ON v.VisitId = r.VisitId + 1
        INNER JOIN Visits_starts s ON v.VisitId = s.VisitId
    )
    
    /* main select; just grouping by visitor and in-time */
    SELECT
      VisitorId,
      InTime,
      OutTime = MAX(OutTime)
    FROM Visits_rec
    GROUP BY VisitorId, InTime
    ORDER BY 1, 2
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多