Rails 및 PostgreSQL에서 시간대를 모두 무시


164

Rails와 Postgres의 날짜와 시간을 다루고 있으며이 문제가 발생합니다.

데이터베이스는 UTC입니다.

사용자는 Rails 앱에서 시간대를 선택하지만 사용자가 시간을 비교하기 위해 현지 시간을 가질 때만 사용됩니다.

사용자는 시간을 저장합니다 (예 : 2012 년 3 월 17 일 오후 7시). 시간대 변환이나 시간대를 저장하고 싶지 않습니다. 날짜와 시간을 절약하고 싶습니다. 이렇게하면 사용자가 시간대를 변경 한 경우 여전히 2012 년 3 월 17 일 오후 7 시가 표시됩니다.

사용자가 지정한 시간대 만 사용하여 사용자 현지 시간대의 현재 시간을 '이전'또는 '이후'로 가져옵니다.

현재 '표준 시간대가없는 타임 스탬프'를 사용하고 있지만 레코드를 검색하면 레일 (?)이 앱의 표준 시간대로 변환되어 원하지 않습니다.

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

데이터베이스의 레코드가 UTC로 나오는 것처럼 보이기 때문에 현재 시간을 내고 'Date.strptime (str, "% m / % d / % Y")'로 시간대를 제거한 다음 내 작업을 수행해야합니다. 그와 함께 쿼리 :

.where("time >= ?", date_start)

모든 시간대를 무시하는 쉬운 방법이 있어야합니다. 어떤 아이디어?

답변:


347

데이터 유형 timestamp은의 짧은 이름입니다 timestamp without time zone.
다른 옵션 timestamptz은 짧습니다 timestamp with time zone.

timestamptz는 IS 원하는 날짜 / 시간 가족 유형은 그대로. 그것은 한 typispreferred설정 pg_type관련 수있는 :

내부 저장 및 시대

내부적으로 타임 스탬프는 디스크와 RAM에서 8 바이트 의 스토리지를 차지 합니다. Postgres epoch (2000-01-01 00:00:00 UTC)의 마이크로 초 수를 나타내는 정수 값입니다.

Postgres는 또한 UNIX 시대 (1970-01-01 00:00:00 UTC)에서 일반적으로 사용되는 UNIX 시간 계산 초 에 대한 기본 지식 을 갖추고 있으며이를 함수 to_timestamp(double precision)또는 에서 사용합니다 EXTRACT(EPOCH FROM timestamptz).

소스 코드 :

* 타임 스탬프와 간격의 h / m / s 필드는 다음과 같이 저장됩니다
* 마이크로 초 단위의 int64 값 (한때 그들은  
* 초 단위의 이중 값)

과:

/ * Unix 및 Postgres 계산에서 0 일의 율리우스 력 날짜 * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

마이크로 초 해상도는 초 동안 최대 6 개의 소수 자릿수로 변환됩니다.

timestamp

Postgres에 표준 시간대가 명시 적으로 제공되지 않았 음 을 알리는 값으로 입력 합니다. 현재 시간대가 가정됩니다. Postgres 실수로 추가 한 시간대 수정자를 무시 합니다!timestamp [without time zone]

표시 시간이 변경되지 않습니다. 동일한 시간대 설정으로 모든 것이 정상입니다. 다른 시간대 설정의 경우 의미가 변경되지만 표시 는 동일하게 유지됩니다.

timestamptz

의 취급은 timestamp with time zone미묘하게 다르다. 나는 여기에 매뉴얼을 인용한다 :

의 경우 timestamp with time zone내부 저장된 값은 항상 UTC (Universal Coordinated Time ...)입니다.

대담한 강조 광산. 시간대 자체는 저장되지 않습니다 . UTC 타임 스탬프를 계산하는 데 사용되는 입력 수정 자이며, 추가 된 표준 시간대 오프셋과 함께 저장 될 로컬 시간을 계산하는 데 사용되는 출력 수정 자입니다. timestamptz입력시 오프셋을 추가하지 않으면 세션의 현재 시간대 설정이 가정됩니다. 모든 계산은 UTC 타임 스탬프 값으로 수행됩니다. 둘 이상의 시간대를 처리해야하거나해야 할 경우을 사용하십시오 timestamptz.

psql 또는 pgAdmin과 같은 클라이언트 또는 libpq 를 통해 통신하는 모든 응용 프로그램 (예 : pg gem이있는 Ruby)에는 현재 시간대에 대한 타임 스탬프와 오프셋이 표시 되거나 요청 된 시간대 에 따라 표시됩니다 (아래 참조). 항상 같은 시점이며 표시 형식 만 다릅니다. 또는 매뉴얼에 따르면 :

모든 시간대 인식 날짜 및 시간은 내부적으로 UTC로 저장됩니다. 클라이언트에 표시되기 전에 TimeZone 구성 매개 변수로 지정된 영역에서 현지 시간으로 변환 됩니다.

이 간단한 예제 (psql)를 고려하십시오.

db = # SELECT 타임 스탬프 '2012-03-05 20:00 +03 ';
      타임 스탬프
------------------------
 2012-03-05 18:00:00 +01

대담한 강조 광산. 여기 뭔 일 있었 니? 입력 리터럴에 대해
임의의 시간대 오프셋 +3을 선택했습니다 . Postgres에게 이것은 UTC 타임 스탬프를 입력하는 많은 방법 중 하나 일뿐 2012-03-05 17:00:00입니다. 내 테스트에서 현재 시간대 설정 비엔나 / 오스트리아 에 대해 쿼리 결과가 표시 되는데, 이는 겨울철과 여름철에 상쇄 됩니다. 겨울철 에 빠지기 때문입니다.+1+22012-03-05 18:00:00+01

Postgres는 이미이 값을 입력 한 방법을 잊었습니다. 기억하는 것은 가치와 데이터 유형뿐입니다. 십진수와 마찬가지로. numeric '003.4', numeric '3.40'또는 numeric '+3.4'- 동일한 내부 값의 모든 결과.

AT TIME ZONE

이 논리를 이해하자마자 원하는 모든 것을 할 수 있습니다. 현재 누락 된 것은 특정 시간대에 따라 타임 스탬프 리터럴을 해석하거나 나타내는 도구입니다. 그 곳에서 AT TIME ZONE구문이 시작됩니다. 두 가지 사용 사례가 있습니다. timestamptz로 변환 timestamp되고 그 반대도 마찬가지입니다.

UTC로를 입력합니다 timestamptz 2012-03-05 17:00:00+0:

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... 이는 다음과 같습니다.

SELECT timestamptz '2012-03-05 17:00:00 UTC'

EST timestamp(동부 표준시) 와 동일한 시점을 표시하려면 다음을 수행하십시오.

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

AT TIME ZONE 'UTC' 두 번 맞습니다 . 첫 번째는 timestamp값을 형식을 반환하는 (주어진) UTC 타임 스탬프로 해석합니다 timestamptz. 두 번째는 변환 timestamptz받는 사람을 timestamp지정된 타임 존 'EST'에서 - 어떤 시간에 독특한 시점에서 시간대 EST 표시에 시계.

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

동일한 UTC 타임 스탬프를 보유한 타임 스탬프 열이있는 8 개 또는 9 개의 동일한 행을 반환 2012-03-05 17:00:00합니다. 9 번째 줄은 내 시간대에서 작동하지만 사악한 함정입니다. 아래를 참조하십시오.

① 표준 시간대 이름 과 표준 시간대 약어 하와이 행 6-8 은 DST (일광 절약 시간제)에 따라 달라 지지만 현재는 다를 수 있습니다. 같은 표준 시간대 이름 'US/Hawaii'은 DST 규칙과 모든 기록 변화를 자동으로 인식하는 반면, 약어 HST는 고정 오프셋을위한 벙어리 코드 일뿐입니다. 여름 / 표준 시간에 다른 약어를 추가해야 할 수도 있습니다. 이름은 올바르게 해석 어떤 주어진 시간대에 타임 스탬프를. 약어는 저렴하지만, 주어진 타임 스탬프에 대한 올바른 하나가 될 필요가있다 :

일광 절약 시간제는 인류가 생각 해낸 가장 밝은 아이디어 중 하나가 아닙니다.

로 표시 ② 행 9, 로드 footgun는 작동 나를 위해 ,하지만 우연의 일치에 의해. 명시 적으로 문자 캐스팅 경우 timestamp [without time zone], 오프셋 어떤 시간대는 무시됩니다 ! 베어 타임 스탬프 만 사용됩니다. 그런 다음 timestamptz예제에서 값이 열 유형과 일치 하도록 자동으로 강제됩니다 . 이 단계에서는 timezone현재 세션 의 설정이 가정됩니다.이 경우 +1내 시간대 (유럽 / 비엔나) 와 동일한 시간대 입니다. 그러나 아마도 귀하의 경우에는 그렇지 않을 것입니다-다른 가치를 초래할 것입니다. 한마디로 : timestamptz리터럴을 캐스트하지 않거나 timestamp시간대 오프셋을 잃습니다.

당신의 질문

사용자는 시간을 저장합니다 (예 : 2012 년 3 월 17 일 오후 7시). 시간대 변환이나 시간대를 저장하고 싶지 않습니다.

시간대 자체는 저장되지 않습니다. 위의 방법 중 하나를 사용하여 UTC 타임 스탬프를 입력하십시오.

사용자가 지정한 시간대 만 사용하여 사용자 현지 시간대의 현재 시간을 '이전'또는 '이후'로 가져옵니다.

다른 시간대의 모든 클라이언트에 대해 하나의 쿼리를 사용할 수 있습니다.
절대 세계 시간 :

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

현지 시계에 따른 시간 :

SELECT * FROM tbl WHERE time_col > now()::time

아직 배경 정보에 질리지 않습니까? 매뉴얼에 더 있습니다.


2
사소한 세부 사항이지만 타임 스탬프는 2000-01-01 이후 마이크로 초 수로 내부적으로 저장되어 있다고 생각합니다- 매뉴얼의 날짜 / 시간 데이터 유형 섹션을 참조하십시오 . 소스를 직접 검사 한 결과 확인 된 것 같습니다. 신기원에 다른 원점을 사용하는 것이 이상합니다!
harmic

2
@harmic 다른 시대에 관해서… 실제로는 그렇게 이상하지 않습니다. 이 Wikipedia 페이지 에는 다양한 컴퓨터 시스템에서 사용되는 24 개의 시대가 나열되어 있습니다. 그동안 유닉스 시대가 일반적이다, 그것만이 아니다.
Basil Bourque

4
@ErwinBrandstetter 하나의 심각한 결함을 제외하고 는 훌륭한 답변입니다. 조화로운 의견으로 Postgres는 Unix 시간을 사용 하지 않습니다 . 문서 에 따르면 : (a) 신기원은 Unix의 1970-01-01이 아닌 2001-01-01이며 (b) 유닉스 시간은 초 단위의 해상도를 갖지만 Postgres는 몇 초를 유지합니다. 소수 자릿수는 컴파일 시간 옵션에 따라 다릅니다. 8 바이트 정수 스토리지 (기본값)를 사용하는 경우 0-6, 부동 소수점 스토리지 (더 이상 사용되지 않음)가 사용되는 경우 0-10입니다.
Basil Bourque

2
@BasilBourque : 이 불행한 실수를 알고 있습니다. 마음에 들지 않으면 편집 할 수 있습니다. 나는 과거에 당신의 대답 중 일부를 보았고 당신은 그것에 능숙합니다. 나에게서 한 번 더 편집하면 커뮤니티 위키에 강제로 추가됩니다. 시간이 지남에 따라 명확하고 포괄적으로 만들기 위해 많은 노력을 기울였습니다.
Erwin Brandstetter

2
수정 : 이전의 의견에서, 나는 Postgres 시대를 2001 년으로 잘못 인용했습니다. 실제로 2000 년 입니다.
Basil Bourque

1

기본적으로 UTC를 처리하려는 경우 :

에 다음 config/application.rb을 추가하십시오.

config.time_zone = 'UTC'

그런 다음 현재 사용자 시간대 이름을 저장하면 current_user.timezone말할 수 있습니다.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone유효한 시간대 이름이어야합니다. 그렇지 않으면 전체 목록을ArgumentError: Invalid Timezone 참조하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.