【问题标题】:Join with conditions and aggregate functions加入条件和聚合函数
【发布时间】:2010-11-30 05:49:54
【问题描述】:

我有一张表,其中记录了进入大门的工程。

DECLARE @doorStatistics TABLE
( id INT IDENTITY,
[user] VARCHAR(250),
accessDate DATETIME,
accessType VARCHAR(5)
)

样本记录:

INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('John Wayne','2009-09-01 07:02:43.000','IN')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('Bruce Willis','2009-09-01 07:12:43.000','IN')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('Bruce Willis','2009-09-01 07:22:43.000','OUT')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('John Wayne','2009-09-01 07:32:43.000','OUT')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('John Wayne','2009-09-01 07:37:43.000','IN')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('Bruce Willis','2009-09-01 07:42:43.000','IN')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('John Wayne','2009-09-01 07:48:43.000','OUT')
INSERT INTO @doorStatistics([user],accessDate,accessType) VALUES ('Bruce Willis','2009-09-01 07:52:43.000','OUT')

我想要做的是一个查询,它给了我以下结果(基于上面的例子):

| user         | date       | inHour   | outHour  |
|--------------|------------|----------|----------|
| John Wayne   | 2009-09-01 | 07:02:43 | 07:48:43 |
| Bruce Willis | 2009-09-01 | 07:12:43 | 07:22:43 |
| John Wayne   | 2009-09-02 | 07:37:43 | 07:48:43 |
| Bruce Willis | 2009-09-02 | 07:42:43 | 07:52:43 |

我的查询如下:

SELECT [user], accessDate AS [in date], 
    (SELECT MIN(accessDate) 
        FROM @doorStatistics ds2 
        WHERE accessType = 'OUT' 
            AND ds2.accessDate > ds.accessDate 
            AND ds.[user] = ds2.[user]) AS [out date] 
FROM @doorStatistics ds 
WHERE accessType = 'IN'

但这并不好,因为当用户忘记注册他/她的入口时,它会产生例如这样的东西:

| user         | date       | inHour   | outHour  |
|--------------|------------|----------|----------|
| John Wayne   | 2009-09-02 | 07:02:43 | 07:48:43 |
| John Wayne   | 2009-09-02 | 07:02:43 | 09:26:43 |

应该是这样的

| user         | date       | inHour   | outHour  |
|--------------|------------|----------|----------|
| John Wayne   | 2009-09-02 | 07:02:43 | 07:48:43 |
| John Wayne   | 2009-09-02 | NULL     | 09:26:43 |

查询不好的第二个原因是性能。我有超过 200 000 条记录,每行的 SELECT 都会减慢查询速度。

可能的解决方案可能是连接两个表

SELECT * FROM @doorStatistics WHERE accessType = 'IN'

与

SELECT * FROM @doorStatistics WHERE accessType = 'OUT'

但我不知道要设置什么条件才能获得正确的日期。也许可以放一些 MAX 或 MIN 函数,但我不知道。

我不想创建临时表并使用游标。

【问题讨论】:

  • 如果布鲁斯·威利斯留宿一夜,你会期待什么结果? (假设 Demi Moore 也在)
  • 如果布鲁斯过夜,outHour 中应该为 NULL。

标签: sql sql-server tsql join


【解决方案1】:

在为具有持续时间的时间事件设计数据库时,最好将“IN”时间和“OUT”时间放在同一行。

您需要做的所有查询都变得如此简单。

参见“Joe Celko's SQL Programming Style”,他在第 48 页和第 154 页谈到了时间凝聚力。

【讨论】:

  • 问题是这个表不是我设计的,我不能改变它的结构。但是,在这种情况下不是很好的解决方案,因为表中的每一行都包含来自一个手指阅读器的数据(对不起,我的英语,但我想你知道我的意思)。有些门有两个(内部和外部)阅读器,有些只有一个(外部)。但无论如何,感谢您提供这本书的链接。
【解决方案2】:

您需要为给定用户的每个 IN 记录选择最小的 OUT 记录,确保没有中间的 IN 记录(这相当于有人在没有离开建筑物的情况下两次进入)。这需要一些适度复杂的 SQL(例如 NOT EXISTS 子句)。因此,您将在表上进行自联接,并在同一张表上加上 NOT EXISTS 子查询。只需确保您对表的所有引用都进行了合理的别名。

【讨论】:

    【解决方案3】:

    在结构层面提高性能:

    • 我建议您将accessDate 列重命名为accessDateTime
    • 然后根据accessDateTime(如下所示)创建一个 PERSISTENT 计算列。然后,您需要的索引将仅包含 accessDate 列,您将使用该列与 user 进行精确比较
    • 确保表格上有正确的索引(从下面的代码中,您可能需要一个关于“user”、“accessDate”并包括“accessType”的索引

    accessDate列定义:

    accessDate AS CONVERT(SMALLDATETIME, CONVERT(CHAR(8), accessDateTime, 112), 112) PERSISTED
    

    现在,鉴于您已经完成并且拥有 SQL-2005+,这个非常长的查询应该可以完成这项工作:

    WITH MatchIN (in_id, out_id)
    AS (SELECT      s.id, CASE WHEN COALESCE(y.id, s.id) = s.id THEN x.id ELSE NULL END
        FROM        @doorStatistics s
        LEFT JOIN   @doorStatistics x
                ON  x.id = (SELECT  TOP 1 z.id
                            FROM    @doorStatistics z
                            WHERE   z."user" = s."user"
                                AND z.accessType = 'OUT'
                                AND z.accessDate =  s.accessDate
                                AND z.accessDateTime >= s.accessDateTime
                            ORDER BY z.accessDateTime ASC
                            )
        LEFT JOIN   @doorStatistics y
                ON  y.id = (SELECT  TOP 1 z.id
                            FROM    @doorStatistics z
                            WHERE   z."user" = s."user"
                                AND z.accessType = 'IN'
                                AND z.accessDate =  s.accessDate
                                AND z.accessDateTime >= s.accessDateTime
                                AND z.accessDateTime <= x.accessDateTime
                            ORDER BY z.accessDateTime DESC
                            )
        WHERE       s.accessType = 'IN'
    )
    ,    MatchOUT (out_id, in_id)
    AS (SELECT      s.id, CASE WHEN COALESCE(y.id, s.id) = s.id THEN x.id ELSE NULL END
        FROM        @doorStatistics s
        LEFT JOIN   @doorStatistics x
                ON  x.id = (SELECT  TOP 1 z.id
                            FROM    @doorStatistics z
                            WHERE   z."user" = s."user"
                                AND z.accessType = 'IN'
                                AND z.accessDate =  s.accessDate
                                AND z.accessDateTime <= s.accessDateTime
                            ORDER BY z.accessDateTime DESC
                            )
        LEFT JOIN   @doorStatistics y
                ON  y.id = (SELECT  TOP 1 z.id
                            FROM    @doorStatistics z
                            WHERE   z."user" = s."user"
                                AND z.accessType = 'OUT'
                                AND z.accessDate =  s.accessDate
                                AND z.accessDateTime <= s.accessDateTime
                                AND z.accessDateTime >= x.accessDateTime
                            ORDER BY z.accessDateTime ASC
                            )
        WHERE       s.accessType = 'OUT'
    )
    
    SELECT  COALESCE(i."user", o."user") AS "user",
            COALESCE(i.accessDate, o.accessDate) AS "date",
            CONVERT(CHAR(10), i.accessDateTime, 108) AS "inHour",
            CONVERT(CHAR(10), o.accessDateTime, 108) AS "outHour"
    FROM   (SELECT in_id, out_id FROM MatchIN
            UNION -- this will eliminate duplicates as the same time
            SELECT in_id, out_id FROM MatchOUT
            ) x
    LEFT JOIN   @doorStatistics i
            ON  i.id = x.in_id
    LEFT JOIN   @doorStatistics o
            ON  o.id = x.out_id
    ORDER BY    "user", "date", "inHour"
    

    要测试缺失行的处理,只需注释掉测试数据的一些 INSERT 语句即可。

    【讨论】:

    • 更改:修复了查询,删除了仅时间计算列,经过测试 - 有效
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-19
    • 2017-11-25
    • 2020-02-27
    • 2014-09-28
    • 1970-01-01
    • 2015-10-29
    相关资源
    最近更新 更多