Mengabaikan zona waktu sama sekali di Rails dan PostgreSQL


164

Saya berurusan dengan tanggal dan waktu di Rails dan Postgres dan mengalami masalah ini:

Basis datanya ada di UTC.

Pengguna menetapkan zona waktu pilihan di aplikasi Rails, tetapi itu hanya digunakan ketika pengguna mendapatkan waktu lokal untuk membandingkan waktu.

Pengguna menyimpan waktu, katakan 17 Maret 2012, 19:00. Saya tidak ingin konversi zona waktu atau zona waktu disimpan. Saya hanya ingin tanggal dan waktu itu disimpan. Dengan begitu jika pengguna mengubah zona waktu mereka, itu masih akan menunjukkan 17 Maret 2012, 19:00.

Saya hanya menggunakan zona waktu yang ditentukan pengguna untuk mendapatkan catatan 'sebelum' atau 'setelah' waktu saat ini di zona waktu lokal pengguna.

Saya saat ini menggunakan 'timestamp tanpa zona waktu' tetapi ketika saya mengambil catatan, rails (?) Mengubahnya menjadi zona waktu di aplikasi, yang saya tidak inginkan.

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

Karena catatan dalam database tampaknya keluar sebagai UTC, peretasan saya adalah untuk mengambil waktu saat ini, hapus zona waktu dengan 'Date.strptime (str, "% m /% d /% Y")' dan kemudian lakukan kueri dengan itu:

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

Sepertinya harus ada cara yang lebih mudah untuk mengabaikan zona waktu di sekitar. Ada ide?

Jawaban:


347

Tipe data timestampadalah nama pendek untuk timestamp without time zone.
Opsi lainnya timestamptzadalah kependekan dari timestamp with time zone.

timestamptzadalah tipe yang disukai dalam keluarga tanggal / waktu, secara harfiah. Ini telah typispreferreddiatur pg_type, yang mungkin relevan:

Penyimpanan internal dan zaman

Secara internal, cap waktu menempati 8 byte penyimpanan pada disk dan dalam RAM. Ini adalah nilai integer yang mewakili hitungan mikrodetik dari zaman Postgres, 2000-01-01 00:00:00 UTC.

Postgres juga memiliki pengetahuan bawaan tentang UNIX penghitungan waktu yang biasa digunakan detik dari zaman UNIX, 1970-01-01 00:00:00 UTC, dan menggunakannya dalam fungsi to_timestamp(double precision)atau EXTRACT(EPOCH FROM timestamptz).

Kode sumber:

* Stempel waktu, serta bidang interval h / m / s, disimpan sebagai
* Nilai int64 dengan satuan mikrodetik. (Dahulu kala mereka  
* nilai ganda dengan satuan detik.)

Dan:

/ * Setara dengan Julian-date pada Hari 0 di perhitungan Unix dan Postgres * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

Resolusi mikrodetik diterjemahkan hingga maksimum 6 digit fraksional selama detik.

timestamp

Nilai yang diketikkan memberitahu Postgres bahwa tidak ada zona waktu yang disediakan secara eksplisit. Zona waktu saat ini diasumsikan. Postgres mengabaikan pengubah zona waktu yang ditambahkan karena kesalahan!timestamp [without time zone]

Tidak ada jam yang digeser untuk tampilan. Dengan pengaturan zona waktu yang sama semuanya baik-baik saja. Untuk zona waktu yang berbeda, pengaturan artinya berubah, tetapi nilai dan tampilan tetap sama.

timestamptz

Penanganannya timestamp with time zoneagak berbeda. Saya mengutip manualnya di sini :

Sebab timestamp with time zone, nilai yang disimpan secara internal selalu dalam UTC (Waktu Koordinasi Universal ...)

Penekanan berani saya. The zona waktu itu sendiri tidak pernah disimpan . Ini adalah pengubah input yang digunakan untuk menghitung stempel waktu UTC yang sesuai, yang disimpan - atau dan pengubah output yang digunakan untuk menghitung waktu lokal untuk ditampilkan - dengan offset zona waktu yang ditambahkan. Jika Anda tidak menambahkan offset untuk timestamptzinput, pengaturan zona waktu sesi saat ini diasumsikan. Semua perhitungan dilakukan dengan nilai timestamp UTC. Jika Anda harus (atau mungkin harus) berurusan dengan lebih dari satu zona waktu, gunakan timestamptz.

Klien seperti psql atau pgAdmin atau aplikasi apa pun yang berkomunikasi melalui libpq (seperti Ruby dengan permata pg) disajikan dengan cap waktu plus offset untuk zona waktu saat ini atau sesuai dengan zona waktu yang diminta (lihat di bawah). Itu selalu merupakan titik waktu yang sama , hanya format tampilan yang bervariasi. Atau, seperti yang dikatakan manual :

Semua tanggal dan waktu sadar zona waktu disimpan secara internal di UTC. Mereka dikonversi ke waktu lokal di zona yang ditentukan oleh parameter konfigurasi TimeZone sebelum ditampilkan ke klien.

Pertimbangkan contoh sederhana ini (dalam psql):

db = # SELECT timestamptz '2012-03-05 20:00 +03 ';
      timestamptz
------------------------
 2012-03-05 18:00:00 +01

Penekanan berani saya. Apa yang terjadi disini?
Saya memilih offset zona waktu sembarang +3untuk input literal. Bagi Postgres, ini hanyalah salah satu dari banyak cara untuk memasukkan stempel waktu UTC 2012-03-05 17:00:00. Hasil kueri ditampilkan untuk pengaturan zona waktu saat ini Wina / Austria dalam pengujian saya, yang memiliki offset +1selama musim dingin dan +2selama musim panas:, 2012-03-05 18:00:00+01karena jatuh ke waktu musim dingin.

Postgres sudah lupa bagaimana nilai ini dimasukkan. Yang diingat hanyalah nilai dan tipe data. Sama seperti dengan angka desimal. numeric '003.4', numeric '3.40'atau numeric '+3.4'- semua menghasilkan nilai internal yang sama persis.

AT TIME ZONE

Segera setelah Anda memahami logika ini, Anda dapat melakukan apa pun yang Anda inginkan. Semua yang hilang sekarang, adalah alat untuk menafsirkan atau mewakili timestamp literal sesuai dengan zona waktu tertentu. Di situlah AT TIME ZONEkonstruk masuk. Ada dua kasus penggunaan yang berbeda. timestamptzdikonversi ke timestampdan sebaliknya.

Untuk memasuki UTC timestamptz 2012-03-05 17:00:00+0:

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

... yang setara dengan:

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

Untuk menampilkan titik waktu yang sama dengan EST timestamp(Waktu Standar Timur):

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

Benar, AT TIME ZONE 'UTC' dua kali . Yang pertama menginterpretasikan timestampnilai sebagai (diberikan) cap waktu UTC mengembalikan tipe timestamptz. Yang kedua mengkonversi timestamptzke timestampdalam zona waktu yang diberikan 'EST' - apa yang ditampilkan jam di zona waktu EST pada titik waktu unik ini.

Contohnya

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);

Mengembalikan 8 (atau 9) baris identik dengan kolom timestamptz yang memegang stempel waktu UTC yang sama 2012-03-05 17:00:00. Jenis baris ke-9 kebetulan bekerja di zona waktu saya, tetapi merupakan perangkap jahat. Lihat di bawah.

① Baris 6 - 8 dengan nama zona waktu dan singkatan zona waktu untuk waktu Hawaii tunduk pada DST (waktu musim panas) dan mungkin berbeda, meskipun saat ini tidak. Nama zona waktu seperti 'US/Hawaii'menyadari aturan DST dan semua pergeseran historis secara otomatis, sedangkan singkatan seperti HSThanyalah kode bodoh untuk offset tetap. Anda mungkin perlu menambahkan singkatan yang berbeda untuk musim panas / waktu standar. The nama dengan benar menafsirkan setiap timestamp pada zona waktu yang diberikan. Sebuah singkatan murah, tapi perlu menjadi orang yang tepat untuk cap waktu yang diberikan:

Daylight Saving Time bukan salah satu ide paling cerdas yang pernah muncul.

② Baris 9, ditandai sebagai footgun dimuat bekerja untuk saya , tetapi hanya karena kebetulan. Jika Anda secara eksplisit memasukkan literal ke timestamp [without time zone], offset zona waktu apa pun diabaikan ! Hanya cap waktu kosong yang digunakan. Nilai tersebut kemudian secara otomatis dipaksa timestamptzdalam contoh untuk mencocokkan jenis kolom. Untuk langkah ini, timezonepengaturan sesi saat ini diasumsikan, yang kebetulan merupakan zona waktu yang sama +1dalam kasus saya (Eropa / Wina). Tetapi mungkin tidak dalam kasus Anda - yang akan menghasilkan nilai yang berbeda. Singkatnya: Jangan melemparkan timestamptzliteral ke timestampatau Anda kehilangan offset zona waktu.

Pertanyaan Anda

Pengguna menyimpan waktu, katakan 17 Maret 2012, 19:00. Saya tidak ingin konversi zona waktu atau zona waktu disimpan.

Zona waktu itu sendiri tidak pernah disimpan. Gunakan salah satu metode di atas untuk memasukkan stempel waktu UTC.

Saya hanya menggunakan zona waktu yang ditentukan pengguna untuk mendapatkan catatan 'sebelum' atau 'setelah' waktu saat ini di zona waktu lokal pengguna.

Anda dapat menggunakan satu permintaan untuk semua klien di zona waktu yang berbeda.
Untuk waktu global absolut:

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

Untuk waktu sesuai dengan jam lokal:

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

Belum bosan dengan informasi latar belakang, belum? Ada lebih banyak di manual.


2
Detail kecil, tapi saya pikir cap waktu disimpan secara internal sebagai jumlah mikrodetik sejak 2000-01-01 - lihat bagian tanggal / waktu datatype manual. Inspeksi saya sendiri terhadap sumber itu sepertinya menegaskan hal itu. Aneh untuk menggunakan asal yang berbeda untuk zaman ini!
berbahaya

2
@armik Adapun zaman yang berbeda ... Sebenarnya tidak begitu aneh. Halaman Wikipedia ini mencantumkan dua lusin zaman yang digunakan oleh berbagai sistem komputer. Sementara zaman Unix adalah umum, itu bukan satu-satunya.
Basil Bourque

4
@ ErwinBrandstetter Ini adalah jawaban yang bagus , kecuali untuk satu kesalahan serius. Sebagai komentar yang berbahaya, Postgres tidak menggunakan waktu Unix. Menurut dokumen : (a) Zaman ini adalah 2001-01-01 daripada Unix '1970-01-01, dan (b) Sementara waktu Unix memiliki resolusi seluruh detik, Postgres menyimpan pecahan detik. Jumlah digit fraksional bergantung pada opsi waktu kompilasi: 0 hingga 6 ketika penyimpanan integer delapan byte (default) digunakan, atau dari 0 hingga 10 saat penyimpanan floating-point (tidak digunakan lagi) digunakan.
Basil Bourque

2
@BasilBourque: Saya sadar akan kesalahan yang tidak menguntungkan ini. Jika Anda tidak keberatan, Anda dapat mengeditnya. Saya telah melihat beberapa jawaban Anda di masa lalu dan Anda bagus dalam hal itu. Satu suntingan lagi dari saya akan memaksakan ini ke komunitas wiki - lama-kelamaan saya telah melakukan banyak upaya (dan pengeditan) untuk membuatnya jelas dan komprehensif.
Erwin Brandstetter

2
KOREKSI: Pada komentar saya sebelumnya, saya salah mengutip zaman Postgres tahun 2001. Sebenarnya ini tahun 2000 .
Basil Bourque

1

Jika Anda ingin berurusan dengan UTC secara default:

Di config/application.rb, tambahkan:

config.time_zone = 'UTC'

Kemudian, jika Anda menyimpan nama zona waktu pengguna saat ini adalah current_user.timezoneAnda dapat mengatakan.

post.created_at.in_time_zone(current_user.timezone)

current_user.timezoneharus menjadi nama zona waktu yang valid, jika tidak Anda akan mendapatkan ArgumentError: Invalid Timezone, lihat daftar lengkap .

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.