У меня есть запрос на создание отчета на основе ряда дат, сгруппированного по date
и employee_id
. Дата должна быть основана на определенном часовом поясе, в данном случае «Азия/Куала-Лумпур». Но это может измениться в зависимости от того, где находится часовой пояс пользователя.
SELECT
d::date AT TIME ZONE 'Asia/Kuala_Lumpur' AS created_date,
e.id,
e.name,
e.division_id,
ARRAY_AGG(
a.id
) as rows,
MIN(a.created_at) FILTER (WHERE a.activity_type = 1) as min_time_in,
MAX(a.created_at) FILTER (WHERE a.activity_type = 2) as max_time_out,
ARRAY_AGG(
CASE
WHEN a.activity_type = 1
THEN a.created_at
ELSE NULL
END
) as check_ins,
ARRAY_AGG(
CASE
WHEN a.activity_type = 2
THEN a.created_at
ELSE NULL
END
) as check_outs
FROM (SELECT MIN(created_at), MAX(created_at) FROM attendance) AS r(startdate,enddate)
, generate_series(
startdate::timestamp,
enddate::timestamp,
interval '1 day') g(d)
CROSS JOIN employee e
LEFT JOIN attendance a ON a.created_at::date = d::date AND e.id = a.employee_id
where d::date = date '2020-11-20' and division_id = 1
GROUP BY
created_date
, e.id
, e.name
, e.division_id
ORDER BY
created_date
, e.id;
Определение и примеры данных для таблицы attendance
:
CREATE TABLE attendance (
id int,
employee_id int,
activity_type int,
created_at timestamp with time zone NOT NULL
);
INSERT INTO attendance VALUES
( 1, 1, 1,'2020-11-18 07:10:25 +00:00'),
( 2, 2, 1,'2020-11-18 07:30:25 +00:00'),
( 3, 3, 1,'2020-11-18 07:50:25 +00:00'),
( 4, 2, 2,'2020-11-18 19:10:25 +00:00'),
( 5, 3, 2,'2020-11-18 19:22:38 +00:00'),
( 6, 1, 2,'2020-11-18 20:01:05 +00:00'),
( 7, 1, 1,'2020-11-19 07:11:23 +00:00'),
( 8, 1, 2,'2020-11-19 16:21:53 +00:00'), <-- Asia/Kuala_Lumpur +8 should be in 20.11 (refer to the check_outs field in the results output)
( 9, 1, 1,'2020-11-19 19:11:23 +00:00'), <-- Asia/Kuala_Lumpur +8 should be in 20.11 (refer to the check_ins field in the results output)
(10, 1, 2,'2020-11-19 20:21:53 +00:00'), <-- Asia/Kuala_Lumpur +8 should be in 20.11 (refer to the check_outs field in the results output)
(11, 1, 1,'2020-11-20 07:41:38 +00:00'),
(12, 1, 2,'2020-11-20 08:52:01 +00:00');
Вот рабочий пример для тестирования.
Запрос не включает в выходные данные строки 8–10 для часового пояса Asia/Kuala_Lumpur +8, хотя должен. Результат показывает поле «строки» 11,12
.
Как я могу исправить запрос, чтобы он генерировал отчет на основе дат заданного часового пояса? (Это означает, что я могу изменить Asia/Kuala_Lumpur
на America/New_York
и т. д.)
Мне сказали сделать что-то вроде этого:
where created_at >= timestamp '2020-11-20' AT TIME ZONE 'Asia/Kuala_Lumpur'
and created_at < timestamp '2020-11-20' AT TIME ZONE 'Asia/Kuala_Lumpur' + interval '1 day'
Но я не уверен, как это применить. Кажется, не работает должным образом в этой скрипке. Он должен включать строки 8,9,10,11,12, но отображаются только строки 8,9,10.
Я обновил, чтобы попробовать даты начала и окончания dbfiddle.uk/…. created_at временная метка с часовым поясом <-- вот как это определено. часовой пояс сервера - "GB", мой собственный сервер - другой часовой пояс. Я хочу, чтобы запрос выводился в соответствии с желаемым часовым поясом.
Рассмотрим некоторые изменения в вашей настройке:
CREATE TABLE employee (
id int PRIMARY KEY -- !
, name text -- do NOT use char(n) !
, division_id int
);
CREATE TABLE attendance (
id int PRIMARY KEY --!
, employee_id int NOT NULL REFERENCES employee -- FK!
, activity_type int
, created_at timestamptz NOT NULL
);
Определение PK упрощает агрегирование строк, потому что PK охватывает всю строку в предложении GROUP BY
. Видеть:
Я бы не использовал «имя» в качестве имени столбца. Это не описательно. Любой другой столбец можно было бы назвать «имя». Учитывать:
SELECT *
FROM ( -- complete employee/date grid for division in range
SELECT g.d::date AS the_date, id AS employee_id, name, division_id
FROM (
SELECT generate_series(MIN(created_at) AT TIME ZONE 'Asia/Kuala_Lumpur'
, MAX(created_at) AT TIME ZONE 'Asia/Kuala_Lumpur'
, interval '1 day')
FROM attendance
) g(d)
CROSS JOIN employee e
WHERE e.division_id = 1
) de
LEFT JOIN ( -- checkins & checkouts per employee/date for division in range
SELECT employee_id, ts::date AS the_date
, array_agg(id) as rows
, min(ts) FILTER (WHERE activity_type = 1) AS min_check_in
, max(ts) FILTER (WHERE activity_type = 2) AS max_check_out
, array_agg(ts::time) FILTER (WHERE activity_type = 1) AS check_ins
, array_agg(ts::time) FILTER (WHERE activity_type = 2) AS check_outs
FROM (
SELECT a.id, a.employee_id, a.activity_type, a.created_at AT TIME ZONE 'Asia/Kuala_Lumpur' AS ts -- convert to timestamp
FROM employee e
JOIN attendance a ON a.employee_id = e.id
-- WHERE a.created_at >= timestamp '2020-11-20' AT TIME ZONE 'Asia/Kuala_Lumpur' -- "sargable" expressions
-- AND a.created_at < timestamp '2020-11-21' AT TIME ZONE 'Asia/Kuala_Lumpur' -- exclusive upper bound (includes all of 2020-11-20);
AND e.division_id = 1
ORDER BY a.employee_id, a.created_at, a.activity_type -- optional to guarantee sorted arrays
) sub
GROUP BY 1, 2
) a USING (the_date, employee_id)
ORDER BY 1, 2;
db<>рабочий пример здесь
Обратите внимание, что мой запрос выводит местную дату и время для Азии/Куала-Лумпур:
test=> SELECT timestamptz '2020-11-20 08:52:01 +0' AT TIME ZONE 'Asia/Kuala_Lumpur' AS local_ts;
local_ts
---------------------
2020-11-20 16:52:01
Когда начать? Вам необходимо понимать концепции часовых поясов и типы данных Postgres timestamp with time zone
(timestamptz
) и timestamp without time zone
(timestamp
). Иначе будет путаница без конца. Начало здесь:
В частности, timestamptz
не хранит часовой пояс:
При простом преобразовании timestamptz
в date
или timestamp
предполагается текущая настройка часового пояса сеанса. Не то, что вы хотите. Укажите часовой пояс явно с помощью конструкции AT TIME ZONE
, чтобы избежать этой ошибки. В вашей скрипке у вас есть оба:
...
, generate_series(
startdate::timestamp AT TIME ZONE 'Asia/Kuala_Lumpur',
enddate::timestamp AT TIME ZONE 'Asia/Kuala_Lumpur',
interval '1 day') g(d)
...
Тоже не делаешь то, что хочешь. После (ошибочного!) приведения к timestamp
конструкция AT TIME ZONE
переводит значения обратно в timestamptz
.
Кроме того, ваш запрос генерирует полное декартово произведение всех пользователей и максимальный диапазон дней в таблице attendance
, только чтобы сократить его до одного дня с помощью:
where created_at >= timestamp '2020-11-20' AT TIME ZONE 'Asia/Kuala_Lumpur'
and created_at < timestamp '2020-11-20' AT TIME ZONE 'Asia/Kuala_Lumpur' + interval '1 day'
Предложение WHERE
, наконец, делает то, что должно. Но нет смысла сначала генерировать полный диапазон дней, чтобы потом отбросить большую его часть. (Кажется, вы тем временем скопировали это из моей другой скрипки?)
Я закомментировал предложение WHERE
и сохранил оптимизированную версию вашего generate_series()
в своем запросе в качестве доказательства концепции. Дальнейшее чтение:
спасибо за довольно подробное объяснение. Я не могу принципиально изменить дизайн БД (на отметку времени), поэтому мне придется придерживаться исходных определений. Возвращаясь к запросу, я могу получить ожидаемый результат на 19-20.11.2020 dbfiddle.uk/…. но мне все еще кажется странным, что в generate_series используются даты за пределами диапазона 20-11-2020 и 21-11-2020. это верно ? что-то выглядит не так.
Что вы имеете в виду под "изменить на timestamptz
"? changed_at
это timestamptz
, я не менял. Кроме того, мой generate_series()
использует не date
, а timestamp
. Однако ваш generate_series()
в обеих новых рабочий примерх использует timestamptz
, что не имеет смысла. Подзапрос a
, который мы присоединяем к выходу timestamp
, а не timestamptz
. Ни MIN
, ни MAX
не имеют здесь никакого смысла.
Вы знаете, что AT TIME ZONE
преобразует timestamp
в timestamptz
и наоборот? Вы читали тему, как я советовал? Вам придется это сделать, иначе путаница никогда не закончится.
timestamptz
является синонимом timestamp with time zone
. Пожалуйста, прочтите: stackoverflow.com/a/9576170/939860Давайте продолжим обсуждение в чате.
Каковы типы столбцов для значений
startdate
иendate
; временная метка, временная метка, что-то еще? Что за серверTimeZone set to? So timestamps are being entered at 'UTC
, верно? Вы пробовали что-то вроде;startdate::timestamp AT TIME ZONE ' Asia/Kuala_Lumpur' , enddate::timestamp AT TIME ZONE ' Asia/Kuala_Lumpur'
?