Обработка generate_series() в запросах с датой или отметкой времени с/без часового пояса

У меня есть запрос на создание отчета на основе ряда дат, сгруппированного по 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.

Каковы типы столбцов для значений 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'?

Adrian Klaver 13.12.2020 20:47

Я обновил, чтобы попробовать даты начала и окончания dbfiddle.uk/…. created_at временная метка с часовым поясом <-- вот как это определено. часовой пояс сервера - "GB", мой собственный сервер - другой часовой пояс. Я хочу, чтобы запрос выводился в соответствии с желаемым часовым поясом.

Axil 14.12.2020 01:47
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
3
2
557
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Дизайн БД

Рассмотрим некоторые изменения в вашей настройке:

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. это верно ? что-то выглядит не так.

Axil 16.12.2020 02:51
dbfiddle.uk/… это еще один я попробовал -10 UTC с Pacific/Honolulu. 19.11.2020 не появляется
Axil 16.12.2020 03:04

Что вы имеете в виду под "изменить на timestamptz"? changed_at это timestamptz, я не менял. Кроме того, мой generate_series() использует не date, а timestamp. Однако ваш generate_series() в обеих новых рабочий примерх использует timestamptz, что не имеет смысла. Подзапрос a, который мы присоединяем к выходу timestamp, а не timestamptz. Ни MIN, ни MAX не имеют здесь никакого смысла.

Erwin Brandstetter 16.12.2020 03:52

Вы знаете, что AT TIME ZONE преобразует timestamp в timestamptz и наоборот? Вы читали тему, как я советовал? Вам придется это сделать, иначе путаница никогда не закончится.

Erwin Brandstetter 16.12.2020 03:56
dbfiddle.uk/… <-- я верю, что это ваша рабочий пример db<> (создано_в отметке времени NOT NULL). Я не могу сделать эту модификацию, так как она живая. Можете ли вы вместо этого использовать существующий дизайн БД?
Axil 16.12.2020 03:56
timestamptz является синонимом timestamp with time zone. Пожалуйста, прочтите: stackoverflow.com/a/9576170/939860
Erwin Brandstetter 16.12.2020 03:58

Давайте продолжим обсуждение в чате.

Axil 16.12.2020 03:59

Другие вопросы по теме