You could use the window function lead()
:
SELECT dt_lead7 AS dt
FROM (
SELECT *, lead(dt, 7) OVER (ORDER BY dt) AS dt_lead7
FROM foo
) d
WHERE dt <= CURRENT_DATE
ORDER BY dt DESC
LIMIT 11;
Somewhat shorter, but the UNION ALL
version will be faster with a suitable index.
That leaves a corner case where "date closest to today" is within the first 7 rows. You can pad the original data with 7 rows of -infinity
to take care of this:
SELECT d.dt_lead7 AS dt
FROM (
SELECT *, lead(dt, 7) OVER (ORDER BY dt) AS dt_lead7
FROM (
SELECT '-infinity'::date AS dt FROM generate_series(1,7)
UNION ALL
SELECT dt FROM foo
) x
) d
WHERE d.dt <= now()::date -- same as: WHERE dt <= now()::date1
ORDER BY d.dt_lead7 DESC -- same as: ORDER BY dt DESC1
LIMIT 11;
I table-qualified the columns in the second query to clarify what happens. See below.
The result will include NULL
values if the "date closest to today" is within the last 7 rows of the base table. You can filter those with an additional sub-select if you need to.
1To address your doubts about output names versus column names in the comments - consider the following quotes from the manual.
Where to use an output column's name:
An output column's name can be used to refer to the column's value in
ORDER BY
and GROUP BY
clauses, but not in the WHERE
or HAVING
clauses;
there you must write out the expression instead.
Bold emphasis mine. WHERE dt <= now()::date
references the column d.dt
, not the the output column of the same name - thereby working as intended.
Resolving conflicts:
If an ORDER BY
expression is a simple name that matches both an output
column name and an input column name, ORDER BY
will interpret it as
the output column name. This is the opposite of the choice that GROUP BY
will make in the same situation. This inconsistency is made to be
compatible with the SQL standard.
Bold emphasis mine again. ORDER BY dt DESC
in the example references the output column's name - as intended. Anyway, either columns would sort the same. The only difference could be with the NULL
values of the corner case. But that falls flat, too, because:
the default behavior is NULLS LAST
when ASC
is specified or implied,
and NULLS FIRST
when DESC
is specified
As the NULL
values come after the biggest values, the order is identical either way.
Without LIMIT
As per request in comment:
WITH x AS (
SELECT *
, row_number() OVER (ORDER BY dt) AS rn
, first_value(dt) OVER (ORDER BY (dt > '2011-11-02')
, dt DESC) AS dt_nearest
FROM foo
)
, y AS (
SELECT rn AS rn_nearest
FROM x
WHERE dt = dt_nearest
)
SELECT dt
FROM x, y
WHERE rn BETWEEN rn_nearest - 3 AND rn_nearest + 7
ORDER BY dt;
If performance is important, I would still go with @Clodoaldo's UNION ALL
variant. It will be fastest. Database agnostic SQL will only get you so far. Other RDBMS do not have window functions at all (yet), or different function names (like first_val
instead of first_value
). You might as well replace LIMIT
with TOP n
(MS SQL) or whatever the local dialect.