Select count of rows that have a certain number of

2019-07-20 15:14发布

问题:

So I'm not quite sure why this one is giving me so much trouble this morning. I'm going to chalk it up to being Friday morning. :) The goal is to count, by frequency, the number of real estate properties in that period that have zero photos, three photos and six or more photos. I need a result set that looks like this (NOTE: the "three column or more should NOT include ones that have exactly six -- there can be a max of six photos):

Period Start      Period End        Zero      ThreeOrMore     ExactlySix
------------------------------------------------------------------------
1/1/2011          1/7/2011          15        132             512
1/8/2011          1/14/2011         44        123             402

I made the dates up, the SQL I have actually produces the correct week start/end dates.

My "Property" table looks like this:

PropertyRecId    Other fields
-----------------------------
12345            <blah>
56789            <blah>

My "PropertyPhoto" table looks like this (only the necessary fields included):

PropertyPhotoId   PropertyRecId     CreatedOn
---------------------------------------------
1                 12345             3/1/2011
2                 12345             3/1/2011
etc...

I'm using the query below but I'm not beholden to it. I just need to accomplish the goal. Keep in mind that with my existing query, I don't have access to the "Propery" record in my top-level select. I tried dropping the queries down into the secondary select where PeriodStart and PeriodEnd are created, but that just caused different issues.

DECLARE @DateStart datetime
DECLARE @DateEnd datetime
SET @DateStart = '1/1/2011'
SET @DateEnd = '12/31/2011'

DECLARE @Frequency varchar(50)
SET @Frequency = 'month'

SELECT
PeriodStart,
PeriodEnd,
??? As Zero,
??? As ThreeOrMore,
??? As ExactlySix
FROM (
SELECT
PeriodStart = CASE @Frequency
WHEN 'day'     THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, CreatedOn))
WHEN 'week'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(DAY, 1 - DATEPART(WEEKDAY, CreatedOn), CreatedOn)))
WHEN 'month'   THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn), 0)))
WHEN 'quarter' THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn), 0)))
WHEN 'year'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn), 0)))
END,
PeriodEnd   = CASE @Frequency
WHEN 'day'     THEN DATEADD(s, -1, DATEADD(day, 1, DATEDIFF(DAY, 0, CreatedOn)))
WHEN 'week'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, 7 - DATEPART(WEEKDAY, CreatedOn), CreatedOn))))
WHEN 'month'   THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn) + 1, 0)))))
WHEN 'quarter' THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn) + 1, 0)))))
WHEN 'year'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn) + 1, 0)))))
END     
FROM Property P
WHERE CreatedOn BETWEEN @DateStart AND @DateEnd
) s
GROUP BY
PeriodStart,
PeriodEnd
ORDER BY PeriodStart

回答1:

You can use the over function in your subquery to get this, and case statements in your outer select to add 'em up:

DECLARE @Frequency varchar(50)
SET @Frequency = 'month'

SELECT
    PeriodStart,
    PeriodEnd,
    SUM(case when s.PhotoCount = 0 then 1 else 0 end) As Zero,
    SUM(case when s.PhotoCount between 3 and 5 then 1 else 0 end) As ThreeOrMore,
    SUM(case when s.PhotoCount = 6 then 1 else 0 end) As ExactlySix
FROM (
    SELECT
        PeriodStart = 
        CASE @Frequency
            WHEN 'day'     THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, CreatedOn))
            WHEN 'week'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(DAY, 1 - DATEPART(WEEKDAY, CreatedOn), CreatedOn)))
            WHEN 'month'   THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn), 0)))
            WHEN 'quarter' THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn), 0)))
            WHEN 'year'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn), 0)))
        END,
        PeriodEnd   = 
        CASE @Frequency
            WHEN 'day'     THEN DATEADD(s, -1, DATEADD(day, 1, DATEDIFF(DAY, 0, CreatedOn)))
            WHEN 'week'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, 7 - DATEPART(WEEKDAY, CreatedOn), CreatedOn))))
            WHEN 'month'   THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn) + 1, 0)))))
            WHEN 'quarter' THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn) + 1, 0)))))
            WHEN 'year'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn) + 1, 0)))))
        END,
        p.PropertyRecId,
        COUNT(ph.PropertyPhotoId) over (partition by p.PropertyRecId) as PhotoCount
    FROM
        Property P
        left join PropertyPhoto ph on
            p.PropertyRecId = ph.PropertyRecId
    WHERE 
        CreatedOn BETWEEN @DateStart AND @DateEnd
) s
GROUP BY
    PeriodStart,
    PeriodEnd
ORDER BY 
    PeriodStart

You can also make your inner query with a traditional group by, but I love me some over and wanted to bring it to your attention:

SELECT
    PeriodStart = 
    CASE @Frequency
        WHEN 'day'     THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, CreatedOn))
        WHEN 'week'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(DAY, 1 - DATEPART(WEEKDAY, CreatedOn), CreatedOn)))
        WHEN 'month'   THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn), 0)))
        WHEN 'quarter' THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn), 0)))
        WHEN 'year'    THEN DATEADD(hour, 0, DATEDIFF(DAY, 0, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn), 0)))
    END,
    PeriodEnd   = 
    CASE @Frequency
        WHEN 'day'     THEN DATEADD(s, -1, DATEADD(day, 1, DATEDIFF(DAY, 0, CreatedOn)))
        WHEN 'week'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, 7 - DATEPART(WEEKDAY, CreatedOn), CreatedOn))))
        WHEN 'month'   THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(MONTH,   DATEDIFF(MONTH,   0, CreatedOn) + 1, 0)))))
        WHEN 'quarter' THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(QUARTER, DATEDIFF(QUARTER, 0, CreatedOn) + 1, 0)))))
        WHEN 'year'    THEN DATEADD(s, -1, DATEADD(hour, 0, DATEDIFF(DAY, -1, DATEADD(DAY, -1, DATEADD(YEAR,    DATEDIFF(YEAR,    0, CreatedOn) + 1, 0)))))
    END,
    p.PropertyRecId,
    COUNT(ph.PropertyPhotoId) over (partition by p.PropertyRecId) as PhotoCount
FROM
    Property P
    left join PropertyPhoto ph on
        p.PropertyRecId = ph.PropertyRecId
WHERE 
    CreatedOn BETWEEN @DateStart AND @DateEnd
GROUP BY
    p.PropertyRecId,
    p.CreatedOn


回答2:

Does this work?:

--1 : Make a lookup-up table of period start/end
CREATE TABLE #PERIODS (
    [Period Start] datetime
  , [Period End]   datetime
);

DECLARE @firstDay datetime
DECLARE @rowid int
SET @firstDay = '2011-01-01'
WHILE @firstDay < '2012-01-01'
BEGIN
    INSERT INTO #PERIODS VALUES ( @firstDay, DATEADD(DAY,6,@firstDay));
    SET @firstDay = @firstDay + 7
END
GO

-- 2 Count the photos per property per period
CREATE TABLE #PHOTOCOUNT (
    [Period Start] datetime
  , [Period End]   datetime
  , PropertyRecId  int
  , PhotoCount     int
);
GO

INSERT INTO #PHOTOCOUNT
SELECT 
    PD.[Period Start]
  , PD.[Period End]   
  , Property.PropertyRecId
  , COUNT(PropertyPhoto.PropertyPhotoId) AS PhotoCount
FROM 
    #PERIODS AS PD,
    Property
    LEFT JOIN PropertyPhoto
        ON Property.PropertyRecId = PropertyPhoto.PropertyRecId 
WHERE 
    PropertyPhoto.PropertyRecId Is Null
    OR PropertyPhoto.CreatedOn BETWEEN PD.[Period Start] AND PD.[Period End]
GROUP BY 
    PD.[Period Start]
  , PD.[Period End]   
  , Property.PropertyRecId;
GO

-- 3 Categorize by 0, 3 to 5, 6
SELECT 
    [Period Start]
  , [Period End]
  , SUM(RANGES.Zero) AS Zero
  , SUM(RANGES.ThreeToFive) AS ThreeToFive
  , SUM(RANGES.Six) AS Six
FROM (
    SELECT 
        [Period Start]
      , [Period End]
      , Zero = CASE WHEN PhotoCount = 0 THEN 1 ELSE 0 END 
      , ThreeToFive = CASE WHEN PhotoCount BETWEEN 3 AND 5 THEN 1 ELSE 0 END 
      , Six = CASE WHEN PhotoCount = 6 THEN 1 ELSE 0 END 
    FROM #PHOTOCOUNT
) RANGES
GROUP BY
    [Period Start]
  , [Period End] 
;

DROP TABLE #PERIODS;
DROP TABLE #PHOTOCOUNT;