根据条件对结果集进行分组

Grouping the result set based on conditions

我正在根据用户的出生日期计算他的年龄。

select UserId, (Convert(int,Convert(char(8),GETDATE(),112))-Convert(char(8),[DateOfBirth],112))/10000 AS [Age] FROM dbo.[User]

这给了我 UserId 和他的年龄。

现在我想对这个结果进行分组。

30 多岁的用户有多少,40 多岁的用户有多少,50 多岁的用户有多少..需要按年龄段计算用户数量

如果用户大于 0 岁且小于 30 岁,则应将他归入 20 岁

如果用户年龄 >= 30 且 < 40,那么他应该被添加到 30 岁的列表中,与 40 岁和 50 岁的一样

这可以在不创建任何临时文件的情况下实现吗table?

您可以使用带有 -1 长度参数和非零函数参数的 round 将值截断为“tens”,并按它分组:

SELECT   UserId, 
         Round((Convert(int,Convert(char(8),GETDATE(),112))-Convert(char(8),[DateOfBirth],112))/10000, -1, 1) AS [Rounded Age],
         Count(*)
FROM     dbo.[User]
GROUP BY Round((Convert(int,Convert(char(8),GETDATE(),112))-Convert(char(8),[DateOfBirth],112))/10000, -1, 1)

是的,你可以。

此查询应该适用于您

SELECT STR(ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) + 's' AS [Age Group], COUNT(UserId) AS Count
FROM dbo.User
GROUP BY STR(ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) + 's'

针对您更新的问题

SELECT CASE 
       WHEN (ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) < 30 THEN '20s' 
       WHEN (ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) >= 50 THEN '50s' 
       ELSE str(ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) + 's' 
    END AS [Age Group], COUNT(UserId) AS Count
FROM dbo.User
GROUP BY CASE 
       WHEN (ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) < 30 THEN '20s' 
       WHEN (ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) >= 50 THEN '50s' 
       ELSE str(ROUND(DATEDIFF(year, DateOfBirth, GETDATE()), - 1) - 10) + 's' 
    END

我会首先将年龄计算放在一个横向连接中,这样就可以很容易地引用它。然后,如果您希望年龄组作为行,您可以加入描述间隔的派生 table:

select v.age_group, count(*) as cnt_users
from dbo.[User] u
cross apply (values 
    ((convert(int, convert(char(8), getdate(),112)) - convert(char(8), u.[DateOfBirth], 112))/10000) 
) a(age)
inner join (values 
    ( 0, 30, '0-30'), 
    (30, 40, '30-40'), 
    (40, 50, '40-50'),
    (50, null, '50+')
) v(min_age, max_age, age_group) 
    on  a.age >= v.min_age
    and (a.age < v.max_age or v.max_age is null)
group by v.age_group

另一方面,如果您想要列中的计数,请使用条件聚合:

select 
    sum(case when a.age < 30 then 1 else 0 end) as age_0_30,
    sum(case when a.age >= 30 and a.age < 40 then 1 else 0 end) as age_30_40,
    sum(case when a.age >= 40 and a.age < 50 then 1 else 0 end) as age_40_50,
    sum(case when a.age >= 50 then 1 else 0 end) as age_50
from dbo.[User] u
cross apply (values 
    ((convert(int, convert(char(8), getdate(),112)) - convert(char(8), [DateOfBirth], 112))/10000) 
) a(age)

我相信这会让你得到你想要的。

任何小于 30 的人都将被放入“20”组。 任何大于等于 50 的都将被放置在“50”组中。

如果他们是 30-39 岁或 40-49 岁,他们将被分到合适的十年组。

SELECT y.AgeDecade, COUNT(*)
FROM dbo.[User] u
    CROSS APPLY (SELECT Age = (CONVERT(int, CONVERT(char(8), GETDATE(), 112)) - CONVERT(int, CONVERT(char(8), u.DateOfBirth, 112))) / 10000) x
    CROSS APPLY (SELECT AgeDecade = CASE
                                        WHEN x.Age <= 29 THEN 20
                                        WHEN x.Age BETWEEN 30 AND 39 THEN 30
                                        WHEN x.Age BETWEEN 40 AND 49 THEN 40
                                        WHEN x.Age >= 50 THEN 50
                                        ELSE NULL
                                    END
    ) y
GROUP BY y.AgeDecade

将逻辑放入 CROSS APPLY 可以更容易地在同一查询中重用逻辑,这样您就可以在 SELECT、GROUP BY、ORDER BY、WHERE 等中使用它,而无需不得不复制它。这也可以使用 cte 来完成,但在这种情况下,这是我的偏好。


更新:

您在评论中询问,当某个年龄组不存在任何人时,如何显示计数为 0。在大多数情况下,答案很简单,LEFT JOIN。与所有事情一样,烤蛋糕的方法总是不止一种。

您可以通过以下几种方式进行操作:

简单的左连接,从我原来的答案中提取查询,然后对 table 进行左连接。您可以通过几种方式执行此操作,CTE、temp table、table 变量、子查询等。要点是,您需要以某种方式隔离您的用户 table。

简单的子查询方法,没什么花哨的。只是将整个查询插入到一个子查询中,然后将其加入我们的新查找 table.

DECLARE @AgeGroup TABLE (AgeGroupID tinyint NOT NULL);
INSERT INTO @AgeGroup (AgeGroupID) VALUES (20),(30),(40),(50);

SELECT ag.AgeGroupID, TotalCount = COUNT(a.AgeDecade)
FROM @AgeGroup ag
    LEFT JOIN (
        SELECT y.AgeDecade
        FROM #User u
            CROSS APPLY (SELECT Age = (CONVERT(int, CONVERT(char(8), GETDATE(), 112)) - CONVERT(int, CONVERT(char(8), u.DateOfBirth, 112))) / 10000) x
            CROSS APPLY (SELECT AgeDecade = CASE
                                                WHEN x.Age <= 29 THEN 20
                                                WHEN x.Age BETWEEN 30 AND 39 THEN 30
                                                WHEN x.Age BETWEEN 40 AND 49 THEN 40
                                                WHEN x.Age >= 50 THEN 50
                                                ELSE NULL
                                            END
            ) y
    ) a ON a.AgeDecade = ag.AgeGroupID
GROUP BY ag.AgeGroupID;

这与使用 cte 完全相同:

DECLARE @AgeGroup TABLE (AgeGroupID tinyint NOT NULL);
INSERT INTO @AgeGroup (AgeGroupID) VALUES (20),(30),(40),(50);

WITH cte_Users AS (
    SELECT y.AgeDecade
    FROM #User u
        CROSS APPLY (SELECT Age = (CONVERT(int, CONVERT(char(8), GETDATE(), 112)) - CONVERT(int, CONVERT(char(8), u.DateOfBirth, 112))) / 10000) x
        CROSS APPLY (SELECT AgeDecade = CASE
                                            WHEN x.Age <= 29 THEN 20
                                            WHEN x.Age BETWEEN 30 AND 39 THEN 30
                                            WHEN x.Age BETWEEN 40 AND 49 THEN 40
                                            WHEN x.Age >= 50 THEN 50
                                            ELSE NULL
                                        END
        ) y
)
SELECT ag.AgeGroupID, TotalCount = COUNT(a.AgeDecade)
FROM @AgeGroup ag
    LEFT JOIN cte_Users a ON a.AgeDecade = ag.AgeGroupID
GROUP BY ag.AgeGroupID;

两者之间的选择纯粹是偏好。此处使用 CTE 没有性能提升。


奖金:

如果你想 table 驱动你的组并且也有 0 个计数,你可以做这样的事情......虽然我会警告你使用 APPLY 运算符要小心,因为它们可能会影响性能有时。

IF OBJECT_ID('tempdb..#User','U') IS NOT NULL DROP TABLE #User; --SELECT * FROM #User
WITH c1 AS (SELECT x.x FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) x(x))       -- 10
    , c2(x) AS (SELECT 1 FROM c1 x CROSS JOIN c1 y)                                     -- 10 * 10
SELECT UserID = IDENTITY(int,1,1)
    , DateOfBirth = CONVERT(date, GETDATE()-(RAND(CHECKSUM(NEWID()))*18500))
INTO #User
FROM c2 u;

IF OBJECT_ID('tempdb..#AgeRange','U') IS NOT NULL DROP TABLE #AgeRange; --SELECT * FROM #AgeRange
CREATE TABLE #AgeRange (
    AgeRangeID  tinyint         NOT NULL IDENTITY(1,1),
    RangeStart  tinyint         NOT NULL,
    RangeEnd    tinyint         NOT NULL,
    RangeLabel  varchar(100)    NOT NULL,
);

INSERT INTO #AgeRange (RangeStart, RangeEnd, RangeLabel)
VALUES ( 0,  29, '< 29')
    ,  (30,  39, '30 - 39')
    ,  (40,  49, '40 - 49')
    ,  (50, 255, '50+');

-- Using an OUTER APPLY
SELECT ar.RangeLabel, COUNT(y.UserID)
FROM #AgeRange ar
    OUTER APPLY (
        SELECT u.UserID
        FROM #User u
            CROSS APPLY (SELECT Age = (CONVERT(int, CONVERT(char(8), GETDATE(), 112)) - CONVERT(int, CONVERT(char(8), u.DateOfBirth, 112))) / 10000) x
        WHERE x.Age BETWEEN ar.RangeStart AND ar.RangeEnd
    ) y
GROUP BY ar.RangeLabel, ar.RangeStart
ORDER BY ar.RangeStart;

-- Using a CTE
WITH cte_users AS (
    SELECT u.UserID
        , Age = (CONVERT(int, CONVERT(char(8), GETDATE(), 112)) - CONVERT(int, CONVERT(char(8), u.DateOfBirth, 112))) / 10000
    FROM #User u
)
SELECT ar.RangeLabel, COUNT(u.UserID)
FROM #AgeRange ar
    LEFT JOIN cte_users u ON u.Age BETWEEN ar.RangeStart AND ar.RangeEnd
GROUP BY ar.RangeStart, ar.RangeLabel
ORDER BY ar.RangeStart;