I have data:
date_key customer_id product_variant_final frequency
2022-03-02 1 a 1
2022-04-01 2 b 2
2022-05-02 3 c 2
......
I want to simplified this query. The logic are:
I want to know the frequency
Select max when frequency 1 and 2, then get column date_pertama and date_kedua
I want to know gap between date_pertama and date_kedua. In there the name is selisih
Categorize selisih based on some condition
I try this query. But maybe it can be simplified.
SELECT customer_id,
product_variant_final,
date_pertama,
date_kedua,
selisih,
CASE WHEN selisih = 0 THEN "Same Day"
WHEN selisih BETWEEN -1 AND -7 THEN "1-7 Days"
WHEN selisih BETWEEN -8 AND -14 THEN "8-14 Days"
WHEN selisih BETWEEN -15 AND -21 THEN "15-21 Days"
WHEN selisih < -21 THEN "21+ Days"
END AS frequency_purchase
FROM(
SELECT customer_id,
product_variant_final,
date_pertama,
date_kedua,
DATE_DIFF(date_pertama, date_kedua, DAY) selisih
FROM(
SELECT customer_id,
product_variant_final,
MAX(CASE WHEN frequency=1 then date_key ELSE NULL END) date_pertama,
MAX(CASE WHEN frequency=2 then date_key ELSE NULL END) date_kedua
FROM(
SELECT date_key,
customer_id,
product_variant_final,
ROW_NUMBER() OVER(PARTITION BY customer_id, product_variant_final ORDER BY date_key) frequency
FROM final_data_variant
WHERE variant_type = "variant"
AND customer_id IN(SELECT customer_id FROM total_purchased_data WHERE total_purchased >= 2)
)
GROUP BY 1,2
))
Your query uses the calculations from the inner SELECT Statement in the outer ones.
Get the first and 2nd date of an order of each customer. Then calculate the difference selisih in days between these dates. Then categorize that value to frequency_purchase.
These nested SELECT structure can be eliminated by using UDF written in SQL, because no further aggregation or joins are done here. The names of the UDF are based on the final calculated columns.
The UDF returns a struct with all needed columns, this is extracted by the (...).*
create temp function frequency_purchase(selisih int64) as
(CASE WHEN selisih>0 THEN "ERROR: selisih must be negativ "
WHEN selisih = 0 THEN "Same Day"
WHEN selisih >= -7 THEN "1-7 Days"
WHEN selisih >= -14 THEN "8-14 Days"
WHEN selisih >= -21 THEN "15-21 Days"
WHEN selisih < -21 THEN "21+ Days"
else "ERROR"
END);
create temp function selisih(dates array<date>) as
(
struct(dates[safe_offset(0)] as date_pertama, dates[safe_offset(1)] as date_kedua,
DATE_DIFF(dates[safe_offset(0)], dates[safe_offset(1)], DAY) as selisih,
frequency_purchase( DATE_DIFF(dates[safe_offset(0)], dates[safe_offset(1)], DAY)) AS frequency_purchase
)
);
WITH final_data_variant as
(SELECT date_sub(current_date(),interval a day) as date_key, cast(rand()*5 as int64) customer_id,1 product_variant_final
from unnest(generate_array(0,60)) a
)
SELECT customer_id,
product_variant_final,
selisih(array_agg(date_key ORDER BY date_key limit 2) ).* # Extract struct
FROM(
SELECT date_key,
customer_id,
product_variant_final
FROM final_data_variant
#WHERE variant_type = "variant" AND customer_id IN(SELECT customer_id FROM total_purchased_data WHERE total_purchased >= 2)
)
GROUP BY 1,2
The Where condition is quoted out, because I do not have the data table for this one.
Related
This question is best asked using an example - if I have daily data (in this case, daily Domestic Box Office for the movie Elvis), how can I sum only the weekend values?
If the data looks like this:
Date
DBO
6/24/2022
12755467
6/25/2022
9929779
6/26/2022
8526333
6/27/2022
4253038
6/28/2022
5267391
6/29/2022
4010762
6/30/2022
3577241
7/1/2022
5320812
7/2/2022
6841224
7/3/2022
6290576
7/4/2022
4248679
7/5/2022
3639110
7/6/2022
3002182
7/7/2022
2460108
7/8/2022
3326066
7/9/2022
4324040
7/10/2022
3530965
I'd like to be able to get results that look like this:
Weekend
DBO Sum
1
31211579
2
18452612
3
11181071
Also - not sure how tricky this would be but would love to include percent change v. last weekend.
Weekend
DBO Sum
% Change
1
31211579
2
18452612
-41%
3
11181071
-39%
I tried this with CASE WHEN but I got the results in different columns, which was not what I was looking for.
SELECT
,SUM(CASE
WHEN DATE BETWEEN '2022-06-24' AND '2022-06-26' THEN index
ELSE 0
END) AS Weekend1
,SUM(CASE
WHEN DATE BETWEEN '2022-07-01' AND '2022-07-03' THEN index
ELSE 0
END) AS Weekend2
,SUM(CASE
WHEN DATE BETWEEN '2022-07-08' AND '2022-07-10' THEN index
ELSE 0
END) AS Weekend3
FROM Elvis
I would start by filtering the data on week-end days only. Then we can group by week to get the index sum ; the last step is to use window functions to compare each week-end with the previous one:
select iso_week,
row_number() over(order by iso_week) weekend_number,
sum(index) as dbo_sum,
( sum(index) - lag(sum(index) over(order by iso_week) )
/ nullif(lag(sum(index)) over(order by iso_week), 0) as ratio_change
from (
select e.*, extract(isoweek from date) iso_week
from elvis e
where extract(dayofweek from date) in (1, 7)
) e
group by iso_week
order by iso_week
Consider below
select *,
round(100 * safe_divide(dbo_sum - lag(dbo_sum) over(order by week), lag(dbo_sum) over(order by week)), 2) change_percent
from (
select extract(week from date + 2) week, sum(dbo) dbo_sum
from your_table
where extract(dayofweek from date + 2) in (1, 2, 3)
group by week
)
if applied to sample data in your question - output is
I am doing a case for a subscription-based company that wants an overview of their monthly sales. This means that I need to sum up the revenue from all active subscriptions, grouped by month. I have managed to work my way through the data to create a table that looks like the one below. Each row is a contract with a start date, end date, proposition (either standard or discount), and the corresponding price per month.
ContractStartDate
ContractEndDate
PropositionReference
PropositionPrice
2018-01-03
NULL
Standard
4.5
2019-01-17
NULL
Discount
2
2018-02-09
2019-01-17
Standard
4.5
...
...
...
...
I want to have the revenue for each month, starting from the minimum ContractStartDate (the first time they received any revenue), for both Standard and Discount propositions. So I would like to have something that looks like this:
Month
RevenueStandard
RevenueDiscount
2017-07
90
30
2017-08
85.5
80
2017-09
180
60
...
2022-10
3862
1136
For each month and each proposition (Standard or Discount), I would need to go over every contract and sum up the proposition price if the startdate was after that month and the enddate was before that month (or there is no enddate).
This is the code I tried, but I feel like I am really far off the solution:
SELECT
MonthYear, PropositionReference,
SUM(CASE WHEN STRFTIME("%m %Y", ContractStartDate) <= MonthYear
AND (ContractEndDate IS NULL OR STRFTIME("%m %Y", ContractEndDate) >= MonthYear)
AND PropositionReference = "Standard"
THEN PropositionPrice ELSE 0 END) AS RevenueStandard,
SUM(CASE WHEN STRFTIME("%m %Y", ContractStartDate) <= MonthYear
AND (ContractEndDate IS NULL OR STRFTIME("%m %Y", ContractEndDate) >= MonthYear)
AND PropositionReference = "Discount"
THEN PropositionPrice ELSE 0 END) AS RevenueDiscount
FROM (SELECT *, STRFTIME("%m %Y", ContractStartDate) AS MonthYear FROM Combined)
GROUP BY MonthYear, PropositionReference
ORDER BY MonthYear, PropositionReference
The following could be the basis if your question has been interpreted correctly.
WITH
range AS (
SELECT min(contractstartdate) AS low, max(coalesce(contractenddate,date('now'))) AS high FROM combined
),
t AS (
SELECT
low AS month,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Standard'
AND strftime('%s',low) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS stnd,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Discount'
AND strftime('%s',low) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS dscnt
FROM range
UNION ALL
SELECT date(month,'+1 month'),
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Standard'
AND strftime('%s',date(month,'+1 month')) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS stnd,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Discount'
AND strftime('%s',date(month,'+1 month')) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS dscnt
FROM t
WHERE date(month,'+1 month') < (SELECT max(coalesce(contractenddate,date('now'))) FROM combined)
LIMIT 500 /* just in case to stop continuous loop */
)
SELECT * FROM t;
So this is
first creating a CTE (Common Table Expression (temp table that exists during the execution)) that consists of a single row with two values the lowest started date and the highest end date (if any null then current date ).
second creating another cte, but a recursive one where the first row is the first month, the second the next month .... until the last month (or in this case for a maximum of 500 iterations (to prevent accidental endless loop)).
each iteration retrieves the sum of the respective type for rows from the combined table that include the date being processed.
As a demo, based upon your data then:-
DROP TABLE IF EXISTS combined;
CREATE TABLE IF NOT EXISTS combined (ContractStartDate TEXT,ContractEndDate TEXT,PropositionReference TEXT,PropositionPrice REAL);
INSERT INTO combined VALUES
('2018-01-03', NULL, 'Standard', 4.5)
,('2019-01-17', NULL, 'Discount', 2)
,('2018-02-09', '2019-01-17', 'Standard', 4.5)
;
WITH
range AS (
SELECT min(contractstartdate) AS low, max(coalesce(contractenddate,date('now'))) AS high FROM combined
),
t AS (
SELECT
low AS month,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Standard'
AND strftime('%s',low) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS stnd,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Discount'
AND strftime('%s',low) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS dscnt
FROM range
UNION ALL
SELECT date(month,'+1 month'),
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Standard'
AND strftime('%s',date(month,'+1 month')) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS stnd,
(
SELECT
coalesce(sum(propositionprice),0)
FROM combined
WHERE propositionreference = 'Discount'
AND strftime('%s',date(month,'+1 month')) BETWEEN strftime('%s',contractStartDate) AND strftime('%s',coalesce(contractEndDate,date('now')))
)
AS dscnt
FROM t
WHERE date(month,'+1 month') < (SELECT max(coalesce(contractenddate,date('now'))) FROM combined)
LIMIT 500 /* just in case to stop continuous loop */
)
SELECT * FROM t;
DROP TABLE IF EXISTS combined;
results in :-
I have a query which get data summarised between two dates like so:
SELECT date(created_at),
COUNT(COALESCE(id, 0)) AS total_orders,
SUM(COALESCE(total_price, 0)) AS total_price,
SUM(COALESCE(taxes, 0)) AS taxes,
SUM(COALESCE(shipping, 0)) AS shipping,
AVG(COALESCE(total_price, 0)) AS average_order_value,
SUM(COALESCE(total_discount, 0)) AS total_discount,
SUM(total_price - COALESCE(taxes, 0) - COALESCE(shipping, 0) - COALESCE(total_discount, 0)) as net_sales
FROM orders
WHERE shop_id = 43
AND orders.active = true
AND orders.created_at >= '2022-07-20'
AND orders.created_at <= '2022-07-26'
GROUP BY date (created_at)
order by created_at::date desc
However for dates that do not have any orders, the query returns nothing and I'd like to return 0.
I have tried with COALESCE but that doesn't seem to do the trick?
Any suggestions?
This should be substantially faster - and correct:
SELECT *
, total_price - taxes - shipping - total_discount AS net_sales -- ⑤
FROM (
SELECT created_at
, COALESCE(total_orders , 0) AS total_orders
, COALESCE(total_price , 0) AS total_price
, COALESCE(taxes , 0) AS taxes
, COALESCE(shipping , 0) AS shipping
, COALESCE(average_order_value , 0) AS average_order_value
, COALESCE(total_discount , 0) AS total_discount
FROM generate_series(timestamp '2022-07-20' -- ①
, timestamp '2022-07-26'
, interval '1 day') AS g(created_at)
LEFT JOIN ( -- ③
SELECT created_at::date
, count(*) AS total_orders -- ⑥
, sum(total_price) AS total_price
, sum(taxes) AS taxes
, sum(shipping) AS shipping
, avg(total_price) AS average_order_value
, sum(total_discount) AS total_discount
FROM orders
WHERE shop_id = 43
AND active -- simpler
AND created_at >= '2022-07-20'
AND created_at < '2022-07-27' -- ② !
GROUP BY 1
) o USING (created_at) -- ④
) sub
ORDER BY created_at DESC;
db<>fiddle here
I copied, simplified, and extended Xu's fiddle for comparison.
① Why this particular form for generate_series()? See:
Generating time series between two dates in PostgreSQL
② Assuming created_at is data type timestamp your original formulation is most probably incorrect. created_at <= '2022-07-26' would include the first instant of '2022-07-26' and exclude the rest. To include all of '2022-07-26', use created_at < '2022-07-27'. See:
How do I write a function in plpgsql that compares a date with a timestamp without time zone?
③ The LEFT JOIN is the core feature of this answer. Generate all days with generate_series(), independently aggregate days from table orders, then LEFT JOIN to retain one row per day like you requested.
④ I made the column name match created_at, so we can conveniently shorten the join syntax with the USING clause.
⑤ Compute net_sales in an outer SELECT after replacing NULL values, so we need COALESCE() only once.
⑥ count(*) is equivalent to COUNT(COALESCE(id, 0)) in any case, but cheaper. See:
Optimizing GROUP BY + COUNT DISTINCT on unnested jsonb column
PostgreSQL: running count of rows for a query 'by minute'
Please refer to the below script.
SELECT *
FROM
(SELECT date(created_at) AS created_at,
COUNT(id) AS total_orders,
SUM(total_price) AS total_price,
SUM(taxes) AS taxes,
SUM(shipping) AS shipping,
AVG(total_price) AS average_order_value,
SUM(total_discount) AS total_discount,
SUM(total_price - taxes - shipping - total_discount) AS net_sales
FROM orders
WHERE shop_id = 43
AND orders.active = true
AND orders.created_at >= '2022-07-20'
AND orders.created_at <= '2022-07-26'
GROUP BY date (created_at)
UNION
SELECT dates AS created_at,
0 AS total_orders,
0 AS total_price,
0 AS taxes,
0 AS shipping,
0 AS average_order_value,
0 AS total_discount,
0 AS net_sales
FROM generate_series('2022-07-20', '2022-07-26', interval '1 day') AS dates
WHERE dates NOT IN
(SELECT created_at
FROM orders
WHERE shop_id = 43
AND orders.active = true
AND orders.created_at >= '2022-07-20'
AND orders.created_at <= '2022-07-26' ) ) a
ORDER BY created_at::date desc;
There is one sample for your reference.
Sample
I got your duplicate test cases at my side. The root cause is created_at field (datattype:timestamp), hence there are duplicate lines.
Below script is correct for your request.
SELECT *
FROM
(SELECT date(created_at) AS created_at,
COUNT(id) AS total_orders,
SUM(total_price) AS total_price,
SUM(taxes) AS taxes,
SUM(shipping) AS shipping,
AVG(total_price) AS average_order_value,
SUM(total_discount) AS total_discount,
SUM(total_price - taxes - shipping - total_discount) AS net_sales
FROM orders
WHERE shop_id = 43
AND orders.active = true
AND orders.created_at >= '2022-07-20'
AND orders.created_at <= '2022-07-26'
GROUP BY date (created_at)
UNION
SELECT dates AS created_at,
0 AS total_orders,
0 AS total_price,
0 AS taxes,
0 AS shipping,
0 AS average_order_value,
0 AS total_discount,
0 AS net_sales
FROM generate_series('2022-07-20', '2022-07-26', interval '1 day') AS dates
WHERE dates NOT IN
(SELECT date (created_at)
FROM orders
WHERE shop_id = 43
AND orders.active = true
AND orders.created_at >= '2022-07-20'
AND orders.created_at <= '2022-07-26' ) ) a
ORDER BY created_at::date desc;
Here is a sample that's same with your side. Link
You can use WITH RECURSIVE to build a table of dates and then select dates that are not in your table
WITH RECURSIVE t(d) AS (
(SELECT '2015-01-01'::date)
UNION ALL
(SELECT d + 1 FROM t WHERE d + 1 <= '2015-01-10')
) SELECT d FROM t WHERE d NOT IN (SELECT d_date FROM tbl);
[look on this post : ][1]
[1]: https://stackoverflow.com/questions/28583379/find-missing-dates-postgresql#:~:text=You%20can%20use%20WITH%20RECURSIVE,SELECT%20d_date%20FROM%20tbl)%3B
I have table with the following columns
id, money, date
I want a SQL query that returns the sum(amount) calculated between two custom dates like
Date between '2014-1-10' and '2016-3-12'
Date between '2015-8-10' and '2017-6-12'
Date between '2014-2-10' and '2016-6-12'
The result will be some thing similar to
customWhere amount
1 20000000$
2 23495000$
3 12940593$
use a select case in the groupby clause
Select (select case
when MyDate between Convert(date,'Some Date')
and Convert(date,'Other Date')
then 1 else 2 end) 'Case',
SUM(Ammount) 'Sum'
FROM YourTable
GROUP BY (select case
when MyDate between Convert(date,'Some Date')
and Convert(date,'Other Date')
then 1 else 2 end)
Group By - https://msdn.microsoft.com/en-us/library/ms177673.aspx
Case - https://msdn.microsoft.com/en-us/library/ms181765.aspx
select 1 as cn, sum(amount) as sss
from table
where Date between '2014-1-10' and '2016-3-12'
union
select 2 as cn, sum(amount) as sss
from table
where Date between '2015-8-10' and '2017-6-12'
...
I have the following table log:
event_time | name |
-------------------------
2014-07-16 11:40 Bob
2014-07-16 10:00 John
2014-07-16 09:20 Bob
2014-07-16 08:20 Bob
2014-07-15 11:20 Bob
2014-07-15 10:20 John
2014-07-15 09:00 Bob
I would like to generate a report, where I can group data by number of entries per day and by entry day. So the resulting report for the table above would be something like this:
event_date | 0-2 | 3 | 4-99 |
-------------------------------
2014-07-16 1 1 0
2014-07-15 2 0 0
I use the following approached to solve it:
Select with grouping in range
How to select the count of values grouped by ranges
If I find answer before anybody post it here, I will share it.
Added
I would like to count a number of daily entries for each name. Then I check to which column this value belongs to, and the I add 1 to that column.
I took it in two steps. Inner query gets the base counts. The outer query uses case statements to sum counts.
SQL Fiddle Example
select event_date,
sum(case when cnt between 0 and 2 then 1 else 0 end) as "0-2",
sum(case when cnt = 3 then 1 else 0 end) as "3",
sum(case when cnt between 4 and 99 then 1 else 0 end) as "4-99"
from
(select cast(event_time as date) as event_date,
name,
count(1) as cnt
from log
group by cast(event_time as date), name) baseCnt
group by event_date
order by event_date
try like this
select da,sum(case when c<3 then 1 else 0 end) as "0-2",
sum(case when c=3 then 1 else 0 end) as "3",
sum(case when c>3 then 1 else 0 end) as "4-66" from (
select cast(event_time as date) as da,count(*) as c from
table1 group by cast(event_time as date),name) as aa group by da
First aggregate in two steps:
SELECT day, CASE
WHEN ct < 3 THEN '0-2'
WHEN ct > 3 THEN '4_or_more'
ELSE '3'
END AS cat
,count(*)::int AS val
FROM (
SELECT event_time::date AS day, count(*) AS ct
FROM tbl
GROUP BY 1
) sub
GROUP BY 1,2
ORDER BY 1,2;
Names should be completely irrelevant according to your description.
Then take the query and run it through crosstab():
SELECT *
FROM crosstab(
$$SELECT day, CASE
WHEN ct < 3 THEN '0-2'
WHEN ct > 3 THEN '4_or_more'
ELSE '3'
END AS cat
,count(*)::int AS val
FROM (
SELECT event_time::date AS day, count(*) AS ct
FROM tbl
GROUP BY 1
) sub
GROUP BY 1,2
ORDER BY 1,2$$
,$$VALUES ('0-2'::text), ('3'), ('4_or_more')$$
) AS f (day date, "0-2" int, "3" int, "4_or_more" int);
crosstab() is supplied by the additional module tablefunc. Details and instructions in this related answer:
PostgreSQL Crosstab Query
This is a variation on a PIVOT query (although PostgreSQL supports this via the crosstab(...) table functions). The existing answers cover the basic technique, I just prefer to construct queries without the use of CASE, where possible.
To get started, we need a couple of things. The first is essentially a Calendar Table, or entries from one (if you don't already have one, they're among the most useful dimension tables). If you don't have one, the entries for the specified dates can easily be generated:
WITH Calendar_Range AS (SELECT startOfDay, startOfDay + INTERVAL '1 DAY' AS nextDay
FROM GENERATE_SERIES(CAST('2014-07-01' AS DATE),
CAST('2014-08-01' AS DATE),
INTERVAL '1 DAY') AS dr(startOfDay))
SQL Fiddle Demo
This is primarily used to create the first step in the double aggregate, like so:
SELECT Calendar_Range.startOfDay, COUNT(Log.name)
FROM Calendar_Range
LEFT JOIN Log
ON Log.event_time >= Calendar_Range.startOfDay
AND Log.event_time < Calendar_Range.nextDay
GROUP BY Calendar_Range.startOfDay, Log.name
SQL Fiddle Demo
Remember that most aggregate columns with a nullable expression (here, COUNT(Log.name)) will ignore null values (not count them). This is also one of the few times it's acceptable to not include a grouped-by column in the SELECT list (normally it makes the results ambiguous). For the actual queries I'll put this into a subquery, but it would also work as a CTE.
We also need a way to construct our COUNT ranges. That's pretty easy too:
Count_Range AS (SELECT text, start, LEAD(start) OVER(ORDER BY start) as next
FROM (VALUES('0 - 2', 0),
('3', 3),
('4+', 4)) e(text, start))
SQL Fiddle Demo
We'll be querying these as "exclusive upper-bound" as well.
We now have all the pieces we need to do the query. We can actually use these virtual tables to make queries in both veins of the current answers.
First, the SUM(CASE...) style.
For this query, we'll take advantage of the null-ignoring qualities of aggregate functions again:
WITH Calendar_Range AS (SELECT startOfDay, startOfDay + INTERVAL '1 DAY' AS nextDay
FROM GENERATE_SERIES(CAST('2014-07-14' AS DATE),
CAST('2014-07-17' AS DATE),
INTERVAL '1 DAY') AS dr(startOfDay)),
Count_Range AS (SELECT text, start, LEAD(start) OVER(ORDER BY start) as next
FROM (VALUES('0 - 2', 0),
('3', 3),
('4+', 4)) e(text, start))
SELECT startOfDay,
COUNT(Zero_To_Two.text) AS Zero_To_Two,
COUNT(Three.text) AS Three,
COUNT(Four_And_Up.text) AS Four_And_Up
FROM (SELECT Calendar_Range.startOfDay, COUNT(Log.name) AS count
FROM Calendar_Range
LEFT JOIN Log
ON Log.event_time >= Calendar_Range.startOfDay
AND Log.event_time < Calendar_Range.nextDay
GROUP BY Calendar_Range.startOfDay, Log.name) Entry_Count
LEFT JOIN Count_Range Zero_To_Two
ON Zero_To_Two.text = '0 - 2'
AND Entry_Count.count >= Zero_To_Two.start
AND Entry_Count.count < Zero_To_Two.next
LEFT JOIN Count_Range Three
ON Three.text = '3'
AND Entry_Count.count >= Three.start
AND Entry_Count.count < Three.next
LEFT JOIN Count_Range Four_And_Up
ON Four_And_Up.text = '4+'
AND Entry_Count.count >= Four_And_Up.start
GROUP BY startOfDay
ORDER BY startOfDay
SQL Fiddle Example
The other option is of course the crosstab query, where the CASE was being used to segment the results. We'll use the Count_Range table to decode the values for us:
SELECT startOfDay, "0 -2", "3", "4+"
FROM CROSSTAB($$WITH Calendar_Range AS (SELECT startOfDay, startOfDay + INTERVAL '1 DAY' AS nextDay
FROM GENERATE_SERIES(CAST('2014-07-14' AS DATE),
CAST('2014-07-17' AS DATE),
INTERVAL '1 DAY') AS dr(startOfDay)),
Count_Range AS (SELECT text, start, LEAD(start) OVER(ORDER BY start) as next
FROM (VALUES('0 - 2', 0),
('3', 3),
('4+', 4)) e(text, start))
SELECT Calendar_Range.startOfDay, Count_Range.text, COUNT(*) AS count
FROM (SELECT Calendar_Range.startOfDay, COUNT(Log.name) AS count
FROM Calendar_Range
LEFT JOIN Log
ON Log.event_time >= Calendar_Range.startOfDay
AND Log.event_time < Calendar_Range.nextDay
GROUP BY Calendar_Range.startOfDay, Log.name) Entry_Count
JOIN Count_Range
ON Entry_Count.count >= Count_Range.start
AND (Entry_Count.count < Count_Range.end OR Count_Range.end IS NULL)
GROUP BY Calendar_Range.startOfDay, Count_Range.text
ORDER BY Calendar_Range.startOfDay, Count_Range.text$$,
$$VALUES('0 - 2', '3', '4+')$$) Data(startOfDay DATE, "0 - 2" INT, "3" INT, "4+" INT)
(I believe this is correct, but don't have a way to test it - Fiddle doesn't seem to have the crosstab functionality loaded. In particular, CTEs probably must go inside the function itself, but I'm not sure....)