
Setelah saya menemukan diri saya berpikir bahwa ketika bekerja dengan waktu dalam database, saya hampir selalu menggunakan waktu yang akurat untuk yang kedua hanya karena saya sudah terbiasa dan bahwa ini adalah opsi yang dijelaskan dalam dokumentasi dan sejumlah besar contoh. Namun, sekarang akurasi ini masih jauh dari cukup untuk semua tugas. Sistem modern rumit - mereka dapat terdiri dari banyak bagian, memiliki jutaan pengguna yang berinteraksi dengan mereka - dan dalam banyak kasus lebih mudah untuk menggunakan akurasi yang lebih besar, yang dukungannya telah ada sejak lama.
Pada artikel ini saya akan berbicara tentang cara-cara menggunakan waktu dengan bagian pecahan sedetik dalam MySQL dan PHP. Itu dipahami sebagai tutorial, sehingga materi ini dirancang untuk berbagai pembaca dan di beberapa tempat mengulangi dokumentasi. Nilai utama harus saya kumpulkan dalam satu teks, semua yang perlu Anda ketahui untuk bekerja dengan waktu seperti itu di MySQL, PHP, dan kerangka kerja Yii, dan juga menambahkan deskripsi masalah yang tidak terlihat yang mungkin Anda temui.
Saya akan menggunakan istilah "waktu presisi tinggi". Dalam dokumentasi MySQL Anda akan melihat istilah "detik fraksional", tetapi terjemahan harfiahnya terdengar aneh, tetapi saya tidak menemukan terjemahan lain yang mapan.
Kapan menggunakan waktu berpresisi tinggi?
Sebagai permulaan, saya akan menunjukkan tangkapan layar kotak masuk dari kotak masuk saya yang menggambarkan ide dengan baik:

Surat adalah reaksi dari orang yang sama terhadap satu peristiwa. Seorang pria tanpa sengaja menekan tombol yang salah, dengan cepat menyadari hal ini dan mengoreksi dirinya sendiri. Akibatnya, kami menerima dua surat yang dikirim pada waktu yang bersamaan, yang penting untuk disortir dengan benar. Jika waktu pengirimannya sama, ada kemungkinan bahwa surat-surat itu akan ditampilkan dalam urutan yang salah dan penerima akan merasa malu, karena dengan demikian ia akan menerima hasil yang salah yang akan ia hitung.
Saya menemukan situasi berikut di mana waktu akurasi tinggi akan relevan:
- Anda ingin mengukur waktu antara beberapa operasi. Semuanya sangat sederhana di sini: semakin tinggi ketepatan cap waktu pada batas interval, semakin tinggi keakuratan hasilnya. Jika Anda menggunakan seluruh detik, maka Anda dapat membuat kesalahan selama 1 detik (jika Anda jatuh di perbatasan detik). Jika Anda menggunakan enam tempat desimal, maka kesalahannya adalah enam urutan lebih rendah.
- Anda memiliki koleksi yang kemungkinan ada beberapa objek dengan waktu pembuatan yang sama. Contohnya adalah obrolan yang akrab bagi semua orang, di mana daftar kontak diurutkan berdasarkan waktu pesan terakhir. Jika muncul navigasi halaman-demi-halaman, maka ada risiko kehilangan kontak di perbatasan halaman. Masalah ini dapat diselesaikan tanpa waktu akurasi tinggi karena pengurutan dan pagination oleh sepasang bidang (waktu + pengidentifikasi unik suatu objek), tetapi solusi ini memiliki kelemahan (setidaknya komplikasi dari query SQL, tetapi tidak hanya itu). Meningkatkan akurasi waktu akan membantu mengurangi kemungkinan masalah dan menghindari kerumitan sistem.
- Anda harus menyimpan riwayat perubahan beberapa objek. Ini sangat penting dalam dunia layanan, di mana modifikasi dapat terjadi secara paralel dan di tempat yang sama sekali berbeda. Sebagai contoh, saya dapat mengutip pekerjaan dengan foto-foto pengguna kami, di mana banyak operasi berbeda dapat dilakukan secara paralel (pengguna dapat membuat foto pribadi atau menghapusnya, dapat dimoderasi di salah satu dari beberapa sistem, dipangkas untuk digunakan sebagai foto dalam obrolan, dll.) )
Harus diingat bahwa seseorang tidak dapat mempercayai nilai yang diperoleh sebesar 100% dan akurasi nyata dari nilai yang diperoleh bisa kurang dari enam tempat desimal. Ini disebabkan oleh fakta bahwa kita bisa mendapatkan nilai waktu yang tidak akurat (terutama ketika bekerja dalam sistem terdistribusi yang terdiri dari banyak server), waktu dapat berubah secara tak terduga (misalnya, saat menyinkronkan melalui NTP atau ketika mengganti jam), dll. Saya tidak akan membahas semua masalah ini, tetapi saya akan memberikan beberapa artikel di mana Anda dapat membaca lebih banyak tentang mereka:
Bekerja dengan waktu presisi tinggi di MySQL
MySQL mendukung tiga jenis kolom di mana waktu dapat disimpan: TIME
, DATETIME
dan TIMESTAMP
. Awalnya, mereka hanya bisa menyimpan nilai yang merupakan kelipatan satu detik (misalnya, 2019-08-14 19:20:21). Dalam versi 5.6.4, yang dirilis pada Desember 2011, menjadi mungkin untuk bekerja dengan bagian fraksional sedetik. Untuk melakukan ini, saat membuat kolom, Anda harus menentukan jumlah tempat desimal, yang harus disimpan di bagian fraksional dari stempel waktu. Jumlah maksimum karakter yang didukung adalah enam, yang memungkinkan Anda untuk menyimpan waktu secara akurat ke mikrodetik. Jika Anda mencoba menggunakan lebih banyak karakter, Anda mendapatkan kesalahan.
Contoh:
Test> CREATE TABLE `ChatContactsList` ( `chat_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, `title` varchar(255) NOT NULL, `last_message_send_time` timestamp(2) NULL DEFAULT NULL ) ENGINE=InnoDB; Query OK, 0 rows affected (0.02 sec) Test> ALTER TABLE `ChatContactsList` MODIFY last_message_send_time TIMESTAMP(9) NOT NULL; ERROR 1426 (42000): Too-big precision 9 specified for 'last_message_send_time'. Maximum is 6. Test> ALTER TABLE `ChatContactsList` MODIFY last_message_send_time TIMESTAMP(3) NOT NULL; Query OK, 0 rows affected (0.09 sec) Records: 0 Duplicates: 0 Warnings: 0 Test> INSERT INTO ChatContactsList (title, last_message_send_time) VALUES ('Chat #1', NOW()); Query OK, 1 row affected (0.03 sec) Test> SELECT * FROM ChatContactsList; +
Dalam contoh ini, stempel waktu dari catatan yang dimasukkan memiliki fraksi nol. Ini terjadi karena nilai input ditunjukkan ke detik terdekat. Untuk memecahkan masalah, keakuratan nilai input harus sama dengan nilai dalam database. Sarannya tampak jelas, tetapi relevan, karena masalah serupa dapat muncul dalam aplikasi nyata: kami dihadapkan pada situasi di mana nilai input memiliki tiga tempat desimal dan enam disimpan dalam database.
Cara termudah untuk mencegah masalah ini terjadi adalah dengan menggunakan nilai input dengan presisi maksimum (hingga mikrodetik). Dalam hal ini, saat menulis data ke tabel, waktu akan dibulatkan ke akurasi yang diperlukan. Ini adalah situasi yang benar-benar normal yang tidak akan menyebabkan peringatan:
Test> UPDATE ChatContactsList SET last_message_send_time="2019-09-22 22:23:15.2345" WHERE chat_id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 Test> SELECT * FROM ChatContactsList; +
Saat menggunakan inisialisasi otomatis dan pembaruan otomatis kolom TIMESTAMP menggunakan struktur formulir DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
penting bahwa nilai memiliki ketepatan yang sama dengan kolom itu sendiri:
Test> ALTER TABLE ChatContactsList ADD COLUMN updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; ERROR 1067 (42000): Invalid default value for 'updated' Test> ALTER TABLE ChatContactsList ADD COLUMN updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6); ERROR 1067 (42000): Invalid default value for 'updated' Test> ALTER TABLE ChatContactsList ADD COLUMN updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); Query OK, 0 rows affected (0.07 sec) Records: 0 Duplicates: 0 Warnings: 0 Test> UPDATE ChatContactsList SET last_message_send_time='2019-09-22 22:22:22' WHERE chat_id=1; Query OK, 0 rows affected (0.00 sec) Rows matched: 1 Changed: 0 Warnings: 0 Test> SELECT * FROM ChatContactsList; +
Fungsi MySQL untuk bekerja dari waktu ke waktu mendukung bekerja dengan bagian fraksional dari unit ukuran. Saya tidak akan mencantumkan semuanya (saya sarankan lihat di dokumentasi), tetapi saya akan memberikan beberapa contoh:
Test> SELECT NOW(2), NOW(4), NOW(4) + INTERVAL 7.5 SECOND; +
Masalah utama yang terkait dengan penggunaan bagian fraksional detik dalam query SQL adalah ketidakkonsistenan akurasi dalam perbandingan ( >
, <
, BETWEEN
). Anda dapat menjumpainya jika data dalam database memiliki satu akurasi, dan dalam kueri - yang lain. Berikut adalah contoh kecil yang menggambarkan masalah ini:
Dalam contoh ini, keakuratan nilai dalam kueri lebih tinggi daripada keakuratan nilai dalam database, dan masalah terjadi "di perbatasan dari atas." Dalam situasi yang berlawanan (jika nilai input memiliki akurasi lebih rendah dari nilai dalam database) tidak akan ada masalah - MySQL akan membawa nilai ke akurasi yang diinginkan di INSERT dan SELECT:
Test> INSERT INTO ChatContactsList (title, last_message_send_time) VALUES ('Chat #3', '2019-09-03 21:20:19.1'); Query OK, 1 row affected (0.00 sec) Test> SELECT title, last_message_send_time FROM ChatContactsList WHERE last_message_send_time <= '2019-09-03 21:20:19.1'; +
Konsistensi akurasi nilai harus selalu diingat ketika bekerja dengan waktu presisi tinggi. Jika masalah batas seperti itu sangat penting bagi Anda, maka Anda perlu memastikan bahwa kode dan basis data bekerja dengan jumlah tempat desimal yang sama.
Pikiran memilih akurasi dalam kolom dengan bagian pecahan detikJumlah ruang yang ditempati oleh bagian fraksional dari satuan waktu tergantung pada jumlah karakter dalam kolom. Tampaknya wajar untuk memilih makna yang akrab: tiga atau enam tempat desimal. Tetapi dalam kasus tiga karakter, itu tidak begitu sederhana. Bahkan, MySQL menggunakan satu byte untuk menyimpan dua tempat desimal:
Persyaratan Penyimpanan Tipe Tanggal dan Waktu
Ternyata jika Anda memilih tiga tempat desimal, maka Anda tidak sepenuhnya memanfaatkan ruang yang ditempati dan untuk overhead yang sama Anda bisa mengambil empat karakter. Secara umum, saya sarankan Anda selalu menggunakan jumlah karakter genap dan, jika perlu, "potong" karakter yang tidak perlu saat menghasilkan. Pilihan yang ideal adalah tidak serakah dan mengambil enam tempat desimal. Dalam kasus terburuk (dengan tipe DATETIME) kolom ini akan menempati 8 byte, yaitu, sama dengan integer di kolom BIGINT.
Lihat juga:
Bekerja dengan waktu presisi tinggi di PHP
Tidak cukup memiliki waktu presisi tinggi dalam basis data - Anda harus dapat bekerja dengannya dalam kode program Anda. Pada bagian ini saya akan berbicara tentang tiga poin utama:
- Menerima dan memformat waktu: Saya akan menjelaskan cara mendapatkan cap waktu sebelum memasukkannya ke dalam database, mendapatkannya dari sana dan melakukan semacam manipulasi.
- Bekerja dengan waktu dalam PDO: Saya akan menunjukkan kepada Anda contoh bagaimana PHP mendukung waktu pemformatan di pustaka database.
- Bekerja dengan waktu dalam kerangka kerja: Saya akan berbicara tentang menggunakan waktu dalam migrasi untuk mengubah struktur basis data.
Ketika bekerja dengan waktu, ada beberapa operasi dasar yang perlu Anda lakukan:
- mendapatkan titik saat ini dalam waktu;
- mendapatkan momen dalam waktu dari beberapa string yang diformat;
- menambahkan periode ke titik waktu (atau mengurangi periode);
- mendapatkan string yang diformat untuk suatu titik waktu.
Pada bagian ini saya akan memberi tahu Anda apa kemungkinan untuk melakukan operasi ini dalam PHP.
Cara pertama adalah bekerja dengan cap waktu sebagai angka . Dalam hal ini, dalam kode PHP kami bekerja dengan variabel numerik, yang kami operasikan melalui fungsi-fungsi seperti time
, date
, strtotime
. Metode ini tidak dapat digunakan untuk bekerja dengan waktu presisi tinggi, karena dalam semua fungsi ini cap waktu adalah bilangan bulat (yang berarti bahwa bagian fraksional di dalamnya akan hilang).
Berikut adalah tanda tangan dari fungsi-fungsi utama dari dokumentasi resmi:
time ( void ) : int
https://www.php.net/manual/ru/function.time.php
strtotime ( string $time [, int $now = time() ] ) : int
http://php.net/manual/ru/function.strtotime.php
date ( string $format [, int $timestamp = time() ] ) : string
https://php.net/manual/ru/function.date.php
strftime ( string $format [, int $timestamp = time() ] ) : string
https://www.php.net/manual/ru/function.strftime.php
Poin yang menarik tentang fungsi tanggalMeskipun tidak mungkin untuk melewatkan bagian pecahan dari satu detik ke input dari fungsi-fungsi ini, di garis templat pemformatan diteruskan ke input dari fungsi date
, Anda dapat mengatur karakter untuk menampilkan milidetik dan mikrodetik. Saat memformat, nol akan selalu dikembalikan pada tempatnya.
Contoh:
$now = time(); print date('Ymd H:i:s.u', $now);
Juga untuk metode ini termasuk hrtime
microtime
dan hrtime
, yang memungkinkan Anda untuk mendapatkan hrtime
waktu dengan bagian fraksional untuk momen saat ini. Masalahnya adalah bahwa tidak ada cara yang siap pakai untuk memformat label seperti itu dan mendapatkannya dari serangkaian format tertentu. Ini dapat diselesaikan dengan mengimplementasikan fungsi-fungsi ini secara independen, tetapi saya tidak akan mempertimbangkan opsi seperti itu.
Jika Anda hanya perlu bekerja dengan timer, maka pustaka HRTime adalah pilihan yang baik, yang saya tidak akan pertimbangkan secara lebih rinci karena keterbatasan penggunaannya. Saya hanya bisa mengatakan bahwa itu memungkinkan Anda untuk bekerja dengan waktu ke nanosecond dan menjamin monoton timer, yang menghilangkan beberapa masalah yang dapat ditemui ketika bekerja dengan perpustakaan lain.
Untuk sepenuhnya bekerja dengan bagian pecahan sedetik, Anda harus menggunakan modul DateTime . Dengan pemesanan tertentu, ini memungkinkan Anda untuk melakukan semua operasi yang tercantum di atas:
Poin tidak jelas saat menggunakan `DateTimeImmutable :: createFromFormat`Huruf u
dalam format string berarti mikrodetik, tetapi juga berfungsi dengan benar jika bagian fraksional kurang presisi. Selain itu, ini adalah satu-satunya cara untuk menentukan bagian pecahan detik dalam string format. Contoh:
$time = \DateTimeImmutable::createFromFormat('Ymd H:i:s.u', '2019-09-12 21:32:43.9085');
Masalah utama dari modul ini adalah ketidaknyamanan ketika bekerja dengan interval yang mengandung detik fraksional (atau bahkan ketidakmungkinan kerja seperti itu). Kelas \DateInterval
meskipun berisi bagian pecahan dari satu detik yang akurat untuk enam tempat desimal yang sama, Anda hanya dapat menginisialisasi bagian fraksional ini melalui DateTime::diff
. Konstruktor kelas DateInterval dan metode pabrik \DateInterval::createFromDateString
hanya dapat bekerja dengan seluruh detik dan tidak memungkinkan untuk menentukan bagian fraksional:
Masalah lain mungkin muncul ketika menghitung perbedaan antara dua titik dalam waktu menggunakan metode \DateTimeImmutable::diff
. Dalam PHP sebelum versi 7.2.12, ada bug karena bagian pecahan dari yang kedua ada secara terpisah dari digit lainnya dan dapat menerima tanda mereka sendiri:
$timeBefore = new \DateTimeImmutable('2019-09-12 21:20:19.987654'); $timeAfter = new \DateTimeImmutable('2019-09-14 12:13:14.123456'); $diff = $timeBefore->diff($timeAfter); print $diff->format('%R%a days %H:%I:%S.%F') . PHP_EOL;
Secara umum, saya menyarankan Anda untuk berhati-hati ketika bekerja dengan interval dan hati-hati menutup kode tersebut dengan tes.
Lihat juga:
Bekerja dengan waktu presisi tinggi di PDO
PDO dan mysqli adalah dua antarmuka utama untuk query database MySQL dari kode PHP. Dalam konteks percakapan tentang waktu, mereka mirip satu sama lain, jadi saya hanya akan berbicara tentang salah satunya - PDO.
Saat bekerja dengan database di PDO, waktu muncul di dua tempat:
- sebagai parameter yang diteruskan ke kueri yang dieksekusi;
- sebagai nilai yang datang sebagai respons terhadap pertanyaan SELECT.
Merupakan praktik yang baik untuk meneruskan placeholder ketika menyampaikan parameter ke permintaan. Placeholder dapat mentransfer nilai dari serangkaian tipe yang sangat kecil: Nilai Boolean, string, dan bilangan bulat. Tidak ada tipe yang cocok untuk tanggal dan waktu, jadi Anda harus secara manual mengonversi nilai dari objek DateTime / DateTimeImmutable kelas ke string.
$now = new \DateTimeImmutable(); $db = new \PDO('mysql:...', 'user', 'password', [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); $stmt = $db->prepare('INSERT INTO Test.ChatContactsList (title, last_message_send_time) VALUES (:title, :date)'); $result = $stmt->execute([':title' => "Test #1", ':date' => $now->format('Ymd H:i:s.u')]);
Menggunakan kode semacam itu sangat tidak nyaman, karena setiap kali Anda perlu memformat nilai yang dikirimkan. Oleh karena itu, dalam basis kode Badoo, kami menerapkan dukungan untuk placeholder yang diketikkan dalam pembungkus kami untuk bekerja dengan database. Dalam hal tanggal, ini sangat nyaman, karena memungkinkan Anda untuk mentransfer nilai dalam format yang berbeda (objek yang mengimplementasikan DateTimeInterface, string yang diformat atau angka dengan cap waktu), dan semua transformasi yang diperlukan dan pemeriksaan kebenaran dari nilai yang ditransfer sudah dilakukan di dalamnya. Sebagai bonus, saat mentransfer nilai yang salah, kami segera belajar tentang kesalahan, dan tidak setelah menerima kesalahan dari MySQL saat menjalankan kueri.
Mengambil data dari hasil kueri terlihat sangat sederhana. Saat melakukan operasi ini, PDO mengembalikan data dalam bentuk string, dan dalam kode kita perlu memproses lebih lanjut hasil jika kita ingin bekerja dengan objek waktu (dan di sini kita memerlukan fungsionalitas untuk mendapatkan waktu dari string yang diformat, yang saya bicarakan di bagian sebelumnya).
$stmt = $db->prepare('SELECT * FROM Test.ChatContactsList ORDER BY last_message_send_time DESC, chat_id DESC LIMIT 5'); $stmt->execute(); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $row['last_message_send_time'] = is_null($row['last_message_send_time']) ? null : new \DateTimeImmutable($row['last_message_send_time']);
Catatan
Fakta bahwa PDO mengembalikan data sebagai string tidak sepenuhnya benar. Saat menerima nilai, dimungkinkan untuk mengatur tipe nilai untuk kolom menggunakan metode PDOStatement::bindColumn
. Saya tidak membicarakan hal ini karena ada rangkaian terbatas jenis yang sama yang tidak membantu kencan.
Sayangnya, ada masalah yang harus diperhatikan. Dalam PHP sebelum versi 7.3, ada bug karena PDO yang mana, ketika atribut PDO::ATTR_EMULATE_PREPARES
"memotong" bagian fraksional detik ketika diterima dari database. Detail dan contoh dapat ditemukan di deskripsi bug di php.net . Di PHP 7.3, kesalahan ini diperbaiki dan memperingatkan bahwa perubahan ini merusak kompatibilitas ke belakang .
Jika Anda menggunakan PHP versi 7.2 atau lebih tua dan tidak dapat memperbaruinya atau mengaktifkan PDO::ATTR_EMULATE_PREPARES
, maka Anda dapat menyiasati bug ini dengan mengoreksi kueri SQL yang mengembalikan waktu dengan bagian fraksional sehingga kolom ini memiliki tipe string. Ini bisa dilakukan, misalnya, seperti ini:
SELECT *, CAST(last_message_send_time AS CHAR) AS last_message_send_time_fixed FROM ChatContactsList ORDER BY last_message_send_time DESC LIMIT 1;
Masalah ini juga dapat ditemui ketika bekerja dengan modul mysqli
: jika Anda menggunakan kueri yang disiapkan melalui panggilan ke metode mysqli::prepare
, maka dalam PHP sebelum versi 7.3, bagian fraksional dari detik tidak akan dikembalikan. Seperti halnya PDO, Anda dapat memperbaikinya dengan memperbarui PHP, atau melewati konversi waktu ke tipe string.
Lihat juga:
Bekerja dengan waktu presisi tinggi di Yii 2
Sebagian besar kerangka kerja modern menyediakan fungsionalitas migrasi yang memungkinkan Anda untuk menyimpan riwayat perubahan skema database dalam kode dan mengubahnya secara bertahap. Jika Anda menggunakan migrasi dan ingin menggunakan waktu presisi tinggi, maka kerangka kerja Anda harus mendukungnya. Untungnya, ini bekerja di luar kotak di semua kerangka kerja utama.
Di bagian ini, saya akan menunjukkan bagaimana dukungan ini diimplementasikan dalam Yii (dalam contoh saya menggunakan versi 2.0.26). Tentang Laravel, Symfony, dan lainnya, saya tidak akan menulis agar tidak membuat artikel ini tidak ada habisnya, tetapi saya akan senang jika Anda menambahkan rincian dalam komentar atau artikel baru tentang topik ini.
Dalam migrasi, kami menulis kode yang menjelaskan perubahan pada skema data. Saat membuat tabel baru, kami menjelaskan semua kolomnya menggunakan metode khusus dari kelas \ yii \ db \ Migration (mereka dideklarasikan di baki SchemaBuilderTrait ). Metode time
, timestamp
dan datetime
yang dapat mengambil nilai input akurasi bertanggung jawab untuk deskripsi kolom yang berisi tanggal dan waktu.
Contoh migrasi tempat tabel baru dibuat dengan kolom waktu presisi tinggi:
use yii\db\Migration; class m190914_141123_create_news_table extends Migration { public function up() { $this->createTable('news', [ 'id' => $this->primaryKey(), 'title' => $this->string()->notNull(), 'content' => $this->text(), 'published' => $this->timestamp(6),
Dan ini adalah contoh migrasi di mana keakuratan dalam kolom yang ada berubah:
class m190916_045702_change_news_time_precision extends Migration { public function up() { $this->alterColumn( 'news', 'published', $this->timestamp(6) ); return true; } public function down() { $this->alterColumn( 'news', 'published', $this->timestamp(3) ); return true; } }
ActiveRecord - : , DateTime-. , — «» PDO::ATTR_EMULATE_PREPARES
. Yii , . , , PDO.
. :
Kesimpulan
, , — , . , , . , !