- หากฟังก์ชันวันที่พื้นฐานของ SQLite ยังไม่เพียงพอ
sqlean-timeจะเพิ่มชนิดข้อมูล Time·Duration และฟังก์ชันวันที่/เวลาในรูปแบบส่วนขยาย พร้อม ความละเอียดระดับนาโนวินาที - ค่า Time ประกอบด้วยจำนวนวินาทีหลัง
0001-01-01 00:00:00 UTCและค่านาโนวินาทีภายในวินาทีปัจจุบัน โดยเมื่อเก็บเป็น BLOB ขนาด 13 ไบต์ จะรองรับช่วงเวลาย้อนหลังและล่วงหน้าได้หลายพันล้านปี - สามารถเก็บเป็น NUMBER 64 บิต ตาม Unix epoch ได้เช่นกัน แต่ยิ่งใช้หน่วยเล็กลง ช่วงที่รองรับก็จะยิ่งแคบลง โดยหน่วยนาโนวินาทีจะแทนค่าได้เพียงตั้งแต่ปี
1678ถึง2262 - API ครอบคลุมการสร้างค่า, การดึงฟิลด์, การแปลง Unix time, การเปรียบเทียบ, เลขคณิต, การตัดเศษ·ปัดเศษ, และการฟอร์แมต·พาร์ส ISO 8601 โดยค่าทั้งหมดจะถูกเก็บและคำนวณเป็น UTC เสมอ
- การคำนวณตามปฏิทินถือว่าทำงานบน ปฏิทินเกรกอเรียน และไม่รองรับ leap second ดังนั้นการบวกวัน·เดือน·ปีควรใช้
time_add_date()แทนtime_add()ที่อิง Duration
โมเดลเวลาของ sqlean-time
sqlean-timeเป็นส่วนขยายที่เพิ่มความสามารถด้าน การจัดการวันที่·เวลาแบบความละเอียดสูง ให้กับ SQLite- ส่วนขยายของ SQLite สามารถเพิ่มได้ด้วยการดาวน์โหลดไฟล์และรันคำสั่งฐานข้อมูลเพียงคำสั่งเดียว
- ส่วนขยายนี้ทำงานโดยยึดค่าหลักอยู่สองชนิด
- Time: ช่วงเวลาหนึ่งจุดในเวลา
- Duration: ช่วงระยะเวลา
การแทนค่า Time และช่วงการจัดเก็บ
- Time ประกอบด้วยคู่ค่า
(seconds, nanoseconds)seconds: จำนวนวินาทีหลัง zero time ที่0001-01-01 00:00:00 UTCในรูป จำนวนเต็ม 64 บิตnanoseconds: ค่านาโนวินาทีภายในวินาทีปัจจุบัน อยู่ในช่วง0-999999999
- หากต้องการความยืดหยุ่นสูงสุด สามารถเก็บค่า Time เป็น BLOB ขนาด 13 ไบต์ ตามรูปแบบภายในได้
- วิธีนี้สามารถแทนวันที่ย้อนหลังและล่วงหน้าได้หลายพันล้านปีด้วยความละเอียดระดับนาโนวินาที
- ยังรองรับการเก็บเป็น NUMBER จำนวนเต็ม 64 บิต ของจำนวนวินาที, มิลลิวินาที, ไมโครวินาที, หรือนาโนวินาที หลัง Unix epoch ที่
1970-01-01 00:00:00 UTC- วินาที: แทนช่วงย้อนหลัง·ล่วงหน้าได้หลายพันล้านปีด้วยความละเอียดระดับวินาที
- มิลลิวินาที: แทนได้ 292 ล้านปีก่อนและหลังปี 1970 ด้วยความละเอียดระดับมิลลิวินาที
- ไมโครวินาที: แทนได้ตั้งแต่ปี
-290307ถึง294246 - นาโนวินาที: แทนได้ตั้งแต่ปี
1678ถึง2262
- Time จะถูกเก็บและคำนวณเป็น UTC เสมอ
- แต่สามารถแปลงกับ offset ของเขตเวลาเฉพาะได้
- การคำนวณตามปฏิทินจะถือว่าใช้ ปฏิทินเกรกอเรียน เสมอ
- ไม่ใช้ leap second
Duration และการสร้างค่า
- Duration คือ จำนวนเต็ม 64 บิตในหน่วยนาโนวินาที
- สามารถแทนช่วงเวลาได้ประมาณ 290 ปี
- เก็บเป็น NUMBER ได้
- เวลาปัจจุบันสร้างได้ด้วย
time_now()- ตัวอย่าง:
time_fmt_iso(time_now())จะคืนสตริง ISO แบบ2024-08-06T21:22:15.431295000Z
- ตัวอย่าง:
- วันที่·เวลาที่ระบุเองสร้างได้ด้วย
time_date()- หากระบุเฉพาะวันที่ จะได้ค่าเป็นเวลาเที่ยงคืน UTC
- สามารถระบุชั่วโมง·นาที·วินาทีและนาโนวินาทีร่วมกันได้
- หากส่ง offset ของเขตเวลาเข้าไป ระบบจะแปลงเป็นเวลา UTC
การดึงฟิลด์วันที่·เวลา
- ฟังก์ชันดึงฟิลด์แต่ละตัวสามารถคืนค่าปี, เดือน, วัน, ชั่วโมง, นาที, วินาที, นาโนวินาที, วันในสัปดาห์, วันของปี, ISO year และ ISO week
- ตัวอย่าง:
time_get_year(),time_get_month(),time_get_day(),time_get_hour(),time_get_minute(),time_get_second(),time_get_nano()
- ตัวอย่าง:
- ฟังก์ชันทั่วไป
time_get()ใช้ดึงค่าด้วยสตริงชื่อฟิลด์- ตัวอย่างค่าที่รองรับ:
millennium,century,decade,year,quarter,month,day - ตัวอย่างหน่วยเวลา:
hour,minute,second,milli,micro,nano - ตัวอย่างเกี่ยวกับ ISO·ปฏิทิน:
isoyear,isoweek,isodow,yearday,weekday - ค่า Unix epoch ดึงได้ด้วย
epoch
- ตัวอย่างค่าที่รองรับ:
การแปลง Unix time
- มีฟังก์ชันสำหรับสร้างค่า Time จาก Unix time
time_unix(seconds)time_unix(seconds, nanoseconds)time_milli(milliseconds)time_micro(microseconds)time_nano(nanoseconds)
- และมีฟังก์ชันสำหรับแปลงค่า Time กลับเป็น Unix time
time_to_unix()time_to_milli()time_to_micro()time_to_nano()
- ระบบปฏิบัติการตระกูล Unix มักบันทึกเวลาเป็นค่าวินาทีแบบ 32 บิต แต่
time_to_unix()จะคืนค่าเป็น 64 บิต- ใช้ได้กับช่วงย้อนหลัง·ล่วงหน้าหลายพันล้านปี
time_to_milli()แทนค่าได้ถึง 292 ล้านปีก่อนและหลังปี 1970time_to_micro()แทนค่าได้ตั้งแต่ปี-290307ถึง294246time_to_nano()แทนค่าได้ตั้งแต่ปี1678ถึง2262
การเปรียบเทียบและเลขคณิต
- ฟังก์ชันเปรียบเทียบเวลาจะใช้ตัดสินลำดับของค่า Time สองค่า
time_after(): คืนค่าว่าเวลาแรกอยู่หลังเวลาที่สองหรือไม่time_before(): คืนค่าว่าเวลาแรกอยู่ก่อนเวลาที่สองหรือไม่time_compare(): ถ้าหลังจะคืน1, ถ้าก่อนจะคืน-1, ถ้าเท่ากันจะคืน0time_equal(): คืนค่าว่าสองค่านี้แทนช่วงเวลาเดียวกันหรือไม่
time_add()ใช้บวก Duration เข้ากับค่า Time- หากใช้ Duration ติดลบก็สามารถใช้เพื่อลบได้
- สามารถใช้ร่วมกับค่าคงที่ Duration เช่น
dur_us(),dur_ms(),dur_s(),dur_m(),dur_h()
- เมื่อจะบวก วัน·เดือน·ปี ต้องใช้
time_add_date()แทนtime_add()time_add_date()ใช้บวกปี, เดือน, วัน และใช้ค่าติดลบเพื่อลบได้
time_sub()จะคืนช่วงเวลาระหว่างค่า Time สองค่าเป็น นาโนวินาทีtime_since()จะคืนเวลาที่ผ่านไปหลังจากเวลาที่กำหนดเป็นนาโนวินาทีtime_until()จะคืนช่วงเวลาที่เหลือจนถึงเวลาที่กำหนดเป็นนาโนวินาที
การตัดเศษและการปัดเศษ
time_trunc()จะปัดค่า Time ลงตามความละเอียดของฟิลด์ที่กำหนด- ตัวอย่างที่รองรับ:
millennium,century,decade,year,quarter,month,week,day,hour,minute,second,milli,micro - ตัวอย่างเช่น เมื่อตัด
2011-11-18T15:56:35.666777888Zที่ระดับhourจะได้2011-11-18T15:00:00Z
- ตัวอย่างที่รองรับ:
- ยังสามารถปัดลงเป็นจำนวนเท่าของ Duration ที่กำหนดได้
- ตัวอย่าง:
12*dur_h(),dur_h(),30*dur_m(),dur_m(),30*dur_s(),dur_s()
- ตัวอย่าง:
time_round()จะปัดไปยังจำนวนเท่าที่ใกล้ที่สุดของ Duration ที่กำหนด- ตัวอย่าง: หากปัด
2011-11-18T15:56:35.666777888Zด้วยdur_h()จะได้2011-11-18T16:00:00Z - หากปัดค่าเดียวกันด้วย
dur_s()จะได้2011-11-18T15:56:36Z
- ตัวอย่าง: หากปัด
การฟอร์แมตและการพาร์ส
time_fmt_iso()จะคืนค่า Time เป็น สตริง ISO 8601- สามารถรับ offset ของเขตเวลาแบบเลือกส่งได้ เพื่อแปลงเป็น offset นั้นก่อนฟอร์แมต
- ค่าที่มีนาโนวินาทีจะแสดงเป็นรูปแบบเช่น
2011-11-18T15:56:35.666777888Z - หากกำหนด offset จะได้รูปแบบเช่น
2011-11-18T18:56:35.666777888+03:00
time_fmt_datetime(),time_fmt_date(),time_fmt_time()จะคืนสตริงแบบ datetime, date, time ตามลำดับ- สามารถรับ offset ของเขตเวลาแบบเลือกส่งได้
time_parse()ใช้พาร์สสตริงที่ฟอร์แมตแล้วให้เป็นค่า Time- สตริง ISO 8601 ที่มีนาโนวินาทีและเขตเวลา
- สตริง ISO 8601 ที่มีนาโนวินาทีและ UTC
Z - สตริง ISO 8601 ที่มีเขตเวลา
- สตริง ISO 8601 แบบ UTC
- วันที่·เวลา UTC รูปแบบ
YYYY-MM-DD HH:MM:SS - วันที่ UTC รูปแบบ
YYYY-MM-DD - เวลา UTC รูปแบบ
HH:MM:SS
- เลย์เอาต์ที่
time_parse()รองรับมี ชุดที่จำกัด
ค่าคงที่ Duration
- มีฟังก์ชันสำหรับคืนค่า Duration ทั่วไปเป็นนาโนวินาที
dur_ns()→1dur_us()→1000dur_ms()→1000000dur_s()→1000000000dur_m()→60000000000dur_h()→3600000000000
พื้นฐานการติดตั้งและการติดตั้งใช้งาน
- ส่วนขยายนี้พัฒนาด้วย C แต่การออกแบบและการพัฒนานั้นอ้างอิงจากแพ็กเกจ time ใน Go standard library อย่างมาก
- แพ็กเกจดังกล่าวใช้ BSD 3-Clause License
- การติดตั้งทำได้โดยดาวน์โหลด latest release แล้วโหลดส่วนขยายใน SQLite CLI
- ตัวอย่าง:
.load ./time - หลังจากโหลดแล้วสามารถใช้คิวรีอย่าง
select time_now();ได้
- ตัวอย่าง:
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
สงสัยว่ารองรับกรณีพิเศษอย่าง การเปลี่ยนแปลงเขตเวลา และความไม่ต่อเนื่องของเวลาท้องถิ่น ที่ Jon Skeet เคยสรุปไว้อย่างโด่งดังหรือไม่
https://stackoverflow.com/questions/6841333/why-is-subtracti...
Computerphile ก็อธิบายเรื่องนี้ได้ดีมากในวิดีโอ 10 นาที
https://www.youtube.com/watch?v=-5wpm-gesOY
ผม/ฉันเคยเรียนรู้มานานแล้วว่าจะไม่ทำไลบรารี วันที่/เวลา หรือ การเข้ารหัสลับ เอง เพราะมี edge case ที่อาจกัดได้อย่างร้ายแรงไม่รู้จบ พอเห็นไลบรารีใหม่แบบนี้จึงมักมองด้วยความกังขา
เอกสารน่าจะชัดเจนกว่านี้ได้ ผู้เขียนพูดว่า “time zones” แต่จริง ๆ ไลบรารีจัดการแค่ offset ของเขตเวลา เท่านั้น เขตเวลาคือสิ่งอย่าง America/New_York ส่วน offset ของเขตเวลาคือส่วนต่างจาก UTC วันนี้นิวยอร์กคือ -14400 วินาที แต่ในอีกไม่กี่เดือนจะเป็น -18000 วินาทีเพราะการเปลี่ยนเวลาออมแสง
น่าสนใจที่มี การแทนค่า/ขนาดของเวลา แตกต่างกันสามแบบ เช่น ไม่แน่ใจว่ามี use case อะไรที่ต้องการความละเอียดระดับนาโนวินาทีในช่วงเวลาหลายพันล้านปี
ที่ชวนสับสนกว่านั้นคือ granularity ของเวลาละเอียดสุดขีด แต่ใน duration ความละเอียดระดับนาโนวินาทีมีช่วงได้แค่ ±290 ปี
แต่ถ้าจะเพิ่ม 2 บิตแล้ว ก็ไม่มีเหตุผลอะไรที่จะไม่เพิ่ม 16 บิตหรือ 32 บิตไปเลย แบบนั้นก็ครอบคลุมตั้งแต่คนที่คำนวณเวลาที่แสงเดินทาง 30 ซม. ไปจนถึงคนที่คำนวณอายุของเอกภพได้
เดาว่าการตัดสินใจออกแบบคงไหลไปประมาณนี้ :)
แน่นอนว่าเป็นเรื่องยากที่จะให้ความแม่นยำต่ำกว่าวินาทีโดยไม่รองรับ leap second และก็ยังไม่ชัดว่าการรองรับ leap second ก่อนอารยธรรมมนุษย์จะมีความหมายอะไร
ออกนอกเรื่องที่เกี่ยวข้องกัน ฐานข้อมูลควรติดตาม หน่วย ได้ ถ้ามีคอลัมน์เวลา ก็ควรประกาศได้ว่าเป็น duration หน่วยวินาทีแบบ float64 เป็นต้น
จากนั้นควรเขียนได้ว่า
SELECT * FROM my_table WHERE duration_s >= 2hแล้วฐานข้อมูลก็แปลง “2h” เป็น 7200.0 วินาทีให้เอง และเปรียบเทียบหน่วยเดียวกันระหว่างสแกนตารางหลายปีก่อนเคยสร้างฐานข้อมูล SQL เฉพาะทางที่มีการจัดการหน่วยแบบ native แบบนี้ แต่ไม่เคยเห็นมาก่อนหรือหลังจากนั้น และดูเหมือนเป็นช่องว่างในระบบนิเวศ UI
ไม่จำเป็นต้องจำกัดแค่เวลา ควรรองรับรายการหน่วยทั้งหมด เช่น มวล ปริมาตร ปริมาณข้อมูล อุณหภูมิ ฯลฯ ฐานข้อมูลยังควรปฏิเสธนิพจน์ที่ไม่สมเหตุสมผลทางคณิตศาสตร์อย่าง
SELECT 2h + 15kg -- type error!ได้ด้วยวิธีนี้จะช่วยมากในการจับข้อผิดพลาดเชิงวิเคราะห์ตั้งแต่เนิ่น ๆ
คิดว่าสำคัญที่จะระบุว่าใช้ จำนวนเต็มมีเครื่องหมาย หรือไม่ อ่านเอกสารแล้วเหมือนจะมีเครื่องหมาย แต่ก็อาจไม่ใช่
ถ้าเป็นจำนวนเต็มมีเครื่องหมาย ก็อาจมี bit string หลายแบบที่แทนวันที่และเวลาเดียวกันได้ ซึ่งไม่ดี
แต่ bit pattern เป็นเรื่องภายในของไลบรารี ถ้าพบ bug ในโค้ดก็ควรชี้ให้เห็น และถ้าเป็นไปได้ก็เสนอการแก้ไขด้วย
ถ้า SQLite3 มี ระบบชนิดข้อมูลที่ขยายได้ ก็คงดีมาก
ระบบชนิดข้อมูลที่ขยายได้ แย่มากต่อประสิทธิภาพของผู้ใช้ปลายทางฐานข้อมูล เพราะจะไม่สามารถใช้ทางลัดอะไรได้เลยในการ parse และ optimize query ต้องตรวจชนิดของ operand ทุกตัว หา implementation ของ operator ที่ถูกต้อง หา index operator family/class ที่ถูกต้อง ฯลฯ ต้องคอย lookup system catalog ของระบบ query ตลอด
การ input/output ของค่าก็ต้องผ่านฟังก์ชันที่เก็บไว้ใน system catalog แม้แต่
select 1ก็ยังตอบไม่ได้โดยไม่ดู system catalogควรมีชุดชนิดข้อมูล built-in ที่เหมาะสม และวิธีประกอบอย่าง struct/JSON ฐานข้อมูลส่วนใหญ่ที่ไม่ใช่ PostgreSQL ทำแบบนี้ และผม/ฉันเชื่ออย่างแรงว่านี่คือทิศทางที่ถูกต้อง
เป็นคำถามแนว Ask HN แบบขี้เกียจนิดหน่อย จากประสบการณ์ อะไรมีประโยชน์หรือมีค่ามากกว่ากัน? การแทนค่าระดับนาโนวินาที หรือการแทนปีที่อยู่นอกช่วงนาโนวินาทีอย่าง 1678~2200?
ไม่ได้ทำงานวิทยาศาสตร์จริงจัง จึงดูเหมือนคุณค่าของนาโนวินาทีจะจำกัดอยู่แค่การทดลองที่ชาญฉลาดมาก ๆ หรือการติดตามธุรกรรมการเงินที่มีช่วงแคบกว่า
ในทางกลับกัน ความสามารถในการแทนวันที่ทางประวัติศาสตร์น่าจะจำเป็นบ่อยกว่า คิดอย่างไร?
แค่ลดความละเอียดลงเหลือ 10 นาโนวินาที ก็ได้ช่วงที่เพียงพอในทางปฏิบัติแล้ว
สงสัยว่าทำไมไม่ใช้ Unix timestamp เป็น signed int64 หน่วยนาโนวินาที แบบสไตล์ Go ถึงจะครอบคลุมหลายล้านปีด้วยความละเอียดระดับนาโนวินาทีไม่ได้ แต่จำเป็นจริงหรือ?
select time_to_nano(time_now());-- 1722979335431295000อยากให้ใช้สำนวนอย่าง “วินาทีหลัง epoch” เฉพาะเมื่อหมายความอย่างนั้นจริง ๆ
สงสัยว่า
select time_sub(time_date(2011, 11, 19), time_date(1311, 11, 18));จะคืนค่าอะไรพอนึกเหตุผลที่ฟังขึ้นได้อยู่บ้าง แต่สิ่งที่สำคัญจริง ๆ คือ “epoch ไหน?” เท่านั้น ในระบบที่อิง UNIX หรือระบบที่พยายามเลียนแบบพฤติกรรมนั้น มันนิยามไว้ชัดเจน แต่เพราะไม่ได้บอกว่าขัดใจตรงไหน จึงยากที่จะโต้แย้งหรืออธิบายว่าทำไมจึงเป็นอย่างตอนนี้
time_date(1311, 11, 18)ไม่ถูกนิยามสำหรับ epoch ที่ระบบคอมพิวเตอร์ส่วนใหญ่ใช้ ดังนั้นผลลัพธ์ใด ๆ ก็เป็นไปได้ ไม่ว่าจะเป็น MAX_INT, MIN_INT, 0, ค่าที่ดูสมเหตุสมผลแต่ไม่สะท้อนการปฏิรูปปฏิทิน, หรือค่าที่แปลงไปยัง epoch อื่นแล้วคำนวณจำนวนวินาทีอย่างถูกต้อง ฯลฯ ก่อน GMT/UTC ทุกอย่างเป็นเวลาท้องถิ่น จึงอาจอ้างได้ว่าไม่มี epoch ที่ใช้ได้แน่นอนว่าการควรรองรับค่าติดลบนั้นถกเถียงได้ทั้งสองทาง เราอาจคาดหวังว่า 24 ชั่วโมงก่อน 1970-1-1 0:00:00 UTC พอดีจะเป็น -86400 แต่คำว่า “since” บอกเป็นนัยค่อนข้างแรงว่าเป็นบวกเท่านั้น
คนอื่นอาจมี epoch ที่ต่างกันโดยสิ้นเชิงด้วยเหตุผลอื่น และถ้าทุกคนในโดเมนที่ใช้ตกลงกันได้ ก็ไม่เป็นไร
หรือมีเหตุผลคัดค้านอื่นอยู่?