Това може да се реши с CTE:
WITH business_days_back AS (
WITH RECURSIVE bd(back_day, go_back) AS (
-- Go back to the previous Monday, allowing for current_date in the weekend
SELECT CASE extract(dow from current_date)
WHEN 0 THEN current_date - 6
WHEN 6 THEN current_date - 5
ELSE current_date - extract(dow from current_date)::int + 1
END,
CASE extract(dow from current_date)
WHEN 0 THEN 7
WHEN 6 THEN 7
ELSE 12 - extract(dow from current_date)::int + 1
END
UNION
-- Go back by the week until go_back = 0
SELECT CASE
WHEN go_back >= 5 THEN back_day - 7
WHEN go_back > 0 THEN back_day - 2 - go_back
END,
CASE
WHEN go_back >= 5 THEN go_back - 5
WHEN go_back > 0 THEN 0
END
FROM bd
)
SELECT back_day FROM bd WHERE go_back = 0
)
SELECT * FROM my_table WHERE analysis_date >= (SELECT * FROM business_days_back);
Някои обяснения:
- Вътрешният CTE започва, като се върне към предходния понеделник, компенсирайки
current_date
който се пада в почивен ден. - След това рекурсивният термин добавя редове, като се връща цели седмици назад (
back_day - 7
за календарната дата иgo_back - 5
за работните дни) доgo_back = 0
. - Външният CTE връща
back_day
дата, къдетоgo_back = 0
. Следователно това е скаларна заявка и можете да я използвате като подзаявка във филтърен израз.
Можете да промените броя на работните дни, за да погледнете назад, като просто промените числата 12
и 7
в началния SELECT
във вътрешния CTE. Имайте предвид обаче, че стойността трябва да е такава, че да се връща към предишния понеделник или заявката ще бъде неуспешна поради същия първоначален SELECT
на вътрешния CTE.
Много по-гъвкаво (и вероятно по-бързо*) решение е да използвате следната функция:
CREATE FUNCTION business_days_diff(from_date date, diff int) RETURNS date AS $$
-- This function assumes Mon-Fri business days
DECLARE
start_dow int;
calc_date date;
curr_diff int;
weekend int;
BEGIN
-- If no diff requested, return the from_date. This may be a non-business day.
IF diff = 0 THEN
RETURN from_date;
END IF;
start_dow := extract(dow from from_date)::int;
calc_date := from_date;
IF diff < 0 THEN -- working backwards
weekend := -2;
IF start_dow = 0 THEN -- Fudge initial Sunday to the previous Saturday
calc_date := calc_date - 1;
start_dow := 6;
END IF;
IF start_dow + diff >= 1 THEN -- Stay in this week
RETURN calc_date + diff;
ELSE -- Work back to Monday
calc_date := calc_date - start_dow + 1;
curr_diff := diff + start_dow - 1;
END IF;
ELSE -- Working forwards
weekend := 2;
IF start_dow = 6 THEN -- Fudge initial Saturday to the following Sunday
calc_date := calc_date + 1;
start_dow := 0;
END IF;
IF start_dow + diff <= 5 THEN -- Stay in this week
RETURN calc_date + diff;
ELSE -- Work forwards to Friday
calc_date := calc_date + 5 - start_dow;
curr_diff := diff - 5 + start_dow;
END IF;
END IF;
-- Move backwards or forwards by full weeks
calc_date := calc_date + (curr_diff / 5) * 7;
-- Process any remaining days, include weekend
IF curr_diff % 5 != 0 THEN
RETURN calc_date + curr_diff % 5 + weekend;
ELSE
RETURN calc_date;
END IF;
END; $$ LANGUAGE plpgsql STRICT IMMUTABLE;
Тази функция може да вземе всяка дата за изчисляване и произволен брой дни в бъдещето (положителна стойност на diff
) или миналото (отрицателна стойност на diff
), включително разликите в рамките на текущата седмица. И тъй като връща датата на работния ден като скалар, използването във вашата заявка е много лесно:
SELECT *
FROM table
WHERE analysis_date >= business_days_diff(current_date, -12);
Освен това можете също да предавате полета от вашата таблица и да правите странни неща като:
SELECT t1.some_value - t2.some_value AS value_diff
FROM table t1
JOIN table t2 ON t2.analysis_date = business_days_diff(t1.analysis_date, -12);
т.е. самостоятелно присъединяване при разделяне на определен брой работни дни.
Имайте предвид, че тази функция предполага работен ден от понеделник до петък в седмицата.
* Тази функция прави само проста аритметика със скаларни стойности. CTE трябва да настрои всякакъв вид структури, за да поддържа итерацията и произтичащите набори от записи.