I have a complicated (to me) SQL query ...
Indeed, you do. But it doesn't have to be that way:
SELECT s.day
,COALESCE(sum(w.reps), 0) AS sum_reps -- assuming reps comes from workouts
,array_agg(e.workout_id) AS ids
FROM exercises e
JOIN workouts w ON w.id = e.workout_id AND w.user_id = 5
RIGHT JOIN (
SELECT now()::date + generate_series(-22, 0) AS day
) s ON s.day = e.created_at::date
GROUP BY 1
ORDER BY 1;
Major points:
RIGHT [OUTER] JOIN
is the reverse twin of LEFT JOIN
. Since joins are applied left-to-right, you don't need parentheses this way.
Never use the basic type and function name date
as identifier. I substituted with day
.
Update: To avoid NULL in the result for the aggregate / window function sum()
use an outer COALESCE
like demonstrated below: COALESCE(sum(reps), 0))
sum(COALESCE(reps, 0))
You don't need to date_trunc()
at all. It's a date
to begin with:
date_trunc('day', s.day)::date AS day
Instead of the complicated and comparatively expensive combination od DISTINCT
+ window functions, you can just use a simple GROUP BY
in this case.
Aggregate functions and COALESCE()
There has been confusion with this in a number of questions recently.
Generally, sum()
or other aggregate functions ignore NULL
values. The result is the same as if the value wasn't there at all. However, there are a number of special cases. The manual advises:
It should be noted that except for count
, these functions return a
null value when no rows are selected. In particular, sum
of no rows
returns null, not zero as one might expect, and array_agg
returns null
rather than an empty array when there are no input rows. The coalesce
function can be used to substitute zero or an empty array for null
when necessary.
This demo should serve to clarify by demonstrating the corner cases:
- 1 table with no rows.
- 3 tables with 1 row holding (
NULL
/ 0
/ 1
)
- 3 tables with 2 row holding
NULL
and (NULL
/ 0
/ 1
)
Test setup
-- no rows
CREATE TABLE t_empty (i int);
-- INSERT nothing
CREATE TABLE t_0 (i int);
CREATE TABLE t_1 (i int);
CREATE TABLE t_n (i int);
-- 1 row
INSERT INTO t_0 VALUES (0);
INSERT INTO t_1 VALUES (1);
INSERT INTO t_n VALUES (NULL);
CREATE TABLE t_0n (i int);
CREATE TABLE t_1n (i int);
CREATE TABLE t_nn (i int);
-- 2 rows
INSERT INTO t_0n VALUES (0), (NULL);
INSERT INTO t_1n VALUES (1), (NULL);
INSERT INTO t_nn VALUES (NULL), (NULL);
Query
SELECT 't_empty' AS tbl
,count(*) AS ct_all
,count(i) AS ct_i
,sum(i) AS simple_sum
,sum(COALESCE(i, 0)) AS inner_coalesce
,COALESCE(sum(i), 0) AS outer_coalesce
FROM t_empty
UNION ALL
SELECT 't_0', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_0
UNION ALL
SELECT 't_1', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_1
UNION ALL
SELECT 't_n', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_n
UNION ALL
SELECT 't_0n', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_0n
UNION ALL
SELECT 't_1n', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_1n
UNION ALL
SELECT 't_nn', count(*), count(i)
,sum(i), sum(COALESCE(i, 0)), COALESCE(sum(i), 0) FROM t_nn;
Result
tbl | ct_all | ct_i | simple_sum | inner_coalesce | outer_coalesce
---------+--------+------+------------+----------------+----------------
t_empty | 0 | 0 | <NULL> | <NULL> | 0
t_0 | 1 | 1 | 0 | 0 | 0
t_1 | 1 | 1 | 1 | 1 | 1
t_n | 1 | 0 | <NULL> | 0 | 0
t_0n | 2 | 1 | 0 | 0 | 0
t_1n | 2 | 1 | 1 | 1 | 1
t_nn | 2 | 0 | <NULL> | 0 | 0
-> SQLfiddle
Ergo, my initial advice was sloppy. You may need COALESCE
with sum()
.
But if you do, use an outer COALESCE
. The the inner COALESCE
in your original query doesn't cover all corner cases and is rarely useful.