ฉันควรเลือกประเภทการประทับเวลาในฐานข้อมูล PostgreSQL


119

ฉันต้องการกำหนดแนวทางปฏิบัติที่ดีที่สุดสำหรับการจัดเก็บการประทับเวลาในฐานข้อมูล Postgres ของฉันในบริบทของโครงการหลายเขตเวลา

ฉันสามารถ

  1. เลือกTIMESTAMP WITHOUT TIME ZONEและจำว่าเขตเวลาใดถูกใช้ในเวลาแทรกสำหรับฟิลด์นี้
  2. เลือกTIMESTAMP WITHOUT TIME ZONEและเพิ่มเขตข้อมูลอื่นซึ่งจะมีชื่อของเขตเวลาที่ใช้ในเวลาแทรก
  3. เลือกTIMESTAMP WITH TIME ZONEและแทรกการประทับเวลาตามนั้น

ฉันมีความต้องการเล็กน้อยสำหรับตัวเลือก 3 (การประทับเวลาพร้อมเขตเวลา) แต่ต้องการความคิดเห็นที่มีความรู้เกี่ยวกับเรื่องนี้

คำตอบ:


142

ก่อนอื่นการจัดการเวลาและเลขคณิตของ PostgreSQL นั้นยอดเยี่ยมมากและตัวเลือกที่ 3 ก็ใช้ได้ดีในกรณีทั่วไป อย่างไรก็ตามเป็นมุมมองที่ไม่สมบูรณ์ของเวลาและเขตเวลาและสามารถเสริมได้:

  1. จัดเก็บชื่อเขตเวลาของผู้ใช้เป็นค่ากำหนดของผู้ใช้ (เช่นAmerica/Los_Angelesไม่ใช่-0700)
  2. มีการส่งข้อมูลเหตุการณ์ / เวลาของผู้ใช้ในพื้นที่ไปยังกรอบอ้างอิง (ส่วนใหญ่จะเป็นการชดเชยจาก UTC เช่น-0700)
  3. ในแอปพลิเคชันแปลงเวลาUTCและจัดเก็บโดยใช้TIMESTAMP WITH TIME ZONEคอลัมน์
  4. ส่งกลับคำขอเวลาท้องถิ่นไปยังเขตเวลาของผู้ใช้ (เช่นแปลงจากUTCเป็นAmerica/Los_Angeles)
  5. ตั้งฐานข้อมูลของคุณไปtimezoneUTC

ตัวเลือกนี้ใช้ไม่ได้เสมอไปเนื่องจากอาจเป็นเรื่องยากที่จะได้รับเขตเวลาของผู้ใช้ดังนั้นคำแนะนำในการป้องกันความเสี่ยงTIMESTAMP WITH TIME ZONEสำหรับการใช้งานที่มีน้ำหนักเบา ที่กล่าวว่าให้ฉันอธิบายลักษณะพื้นฐานบางประการของตัวเลือก 4 นี้โดยละเอียด

เช่นเดียวกับทางเลือกที่ 3 เหตุผลที่WITH TIME ZONEเป็นเพราะเวลาที่สิ่งที่เกิดขึ้นเป็นที่แน่นอนขณะที่ในเวลาที่ WITHOUT TIME ZONEให้เขตเวลาสัมพัทธ์ อย่าเคยผสม TIMESTAMPs สัมบูรณ์และสัมพัทธ์

จากมุมมองแบบเป็นโปรแกรมและความสอดคล้องตรวจสอบให้แน่ใจว่าการคำนวณทั้งหมดทำโดยใช้ UTC เป็นเขตเวลา นี่ไม่ใช่ข้อกำหนดของ PostgreSQL แต่จะช่วยเมื่อรวมเข้ากับภาษาโปรแกรมหรือสภาพแวดล้อมอื่น ๆ การตั้งค่าCHECKบนคอลัมน์เพื่อให้แน่ใจว่าการเขียนไปยังคอลัมน์การประทับเวลามีการชดเชยโซนเวลา0เป็นตำแหน่งการป้องกันที่ป้องกันไม่ให้มีข้อบกพร่องบางคลาส (เช่นสคริปต์ทิ้งข้อมูลไปยังไฟล์และอย่างอื่นจะจัดเรียงข้อมูลเวลาโดยใช้ a เรียงศัพท์). อีกครั้ง PostgreSQL ไม่จำเป็นต้องใช้สิ่งนี้ในการคำนวณวันที่อย่างถูกต้องหรือเพื่อแปลงระหว่างโซนเวลา (เช่น PostgreSQL มีความเชี่ยวชาญในการแปลงเวลาระหว่างสองเขตเวลาใด ๆ โดยพลการ) เพื่อให้แน่ใจว่าข้อมูลที่เข้าสู่ฐานข้อมูลถูกจัดเก็บโดยมีค่าชดเชยเป็นศูนย์:

CREATE TABLE my_tbl (
  my_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
  CHECK(EXTRACT(TIMEZONE FROM my_timestamp) = '0')
);
test=> SET timezone = 'America/Los_Angeles';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
ERROR:  new row for relation "my_tbl" violates check constraint "my_tbl_my_timestamp_check"
test=> SET timezone = 'UTC';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
INSERT 0 1

มันไม่ได้สมบูรณ์แบบ 100% แต่มีมาตรการต่อต้านการเดินเท้าที่แข็งแกร่งเพียงพอที่ทำให้แน่ใจว่าข้อมูลถูกแปลงเป็น UTC แล้ว มีความคิดเห็นมากมายเกี่ยวกับวิธีการทำสิ่งนี้ แต่ดูเหมือนจะดีที่สุดในทางปฏิบัติจากประสบการณ์ของฉัน

การวิพากษ์วิจารณ์เกี่ยวกับการจัดการโซนเวลาของฐานข้อมูลนั้นเป็นสิ่งที่ถูกต้องเป็นส่วนใหญ่ (มีฐานข้อมูลมากมายที่จัดการกับสิ่งนี้โดยไม่มีความสามารถอย่างมาก) อย่างไรก็ตามการจัดการการประทับเวลาและเขตเวลาของ PostgreSQL นั้นยอดเยี่ยมมาก (แม้จะมี "คุณสมบัติ" บางอย่างอยู่ที่นี่ก็ตาม) ตัวอย่างเช่นคุณสมบัติอย่างหนึ่ง:

-- Make sure we're all working off of the same local time zone
test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT NOW();
              now              
-------------------------------
 2011-05-27 15:47:58.138995-07
(1 row)

test=> SELECT NOW() AT TIME ZONE 'UTC';
          timezone          
----------------------------
 2011-05-27 22:48:02.235541
(1 row)

โปรดทราบว่าAT TIME ZONE 'UTC'แถบข้อมูลเขตเวลาและสร้างความสัมพันธ์TIMESTAMP WITHOUT TIME ZONEโดยใช้กรอบอ้างอิงของเป้าหมาย ( UTC)

เมื่อแปลงจากค่าที่ไม่สมบูรณ์TIMESTAMP WITHOUT TIME ZONEเป็นTIMESTAMP WITH TIME ZONEเขตเวลาที่ขาดหายไปจะได้รับการสืบทอดมาจากการเชื่อมต่อของคุณ:

test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
 date_part 
-----------
        -7
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
 date_part 
-----------
        -7
(1 row)

-- Now change to UTC    
test=> SET timezone = 'UTC';
SET
-- Create an absolute time with timezone offset:
test=> SELECT NOW();
              now              
-------------------------------
 2011-05-27 22:48:40.540119+00
(1 row)

-- Creates a relative time in a given frame of reference (i.e. no offset)
test=> SELECT NOW() AT TIME ZONE 'UTC';
          timezone          
----------------------------
 2011-05-27 22:48:49.444446
(1 row)

test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
 date_part 
-----------
         0
(1 row)

test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
 date_part 
-----------
         0
(1 row)

บรรทัดล่าง:

  • จัดเก็บเขตเวลาของผู้ใช้เป็นป้ายกำกับที่ระบุชื่อ (เช่นAmerica/Los_Angeles) และไม่ใช่การชดเชยจาก UTC (เช่น-0700)
  • ใช้ UTC สำหรับทุกสิ่งเว้นแต่จะมีเหตุผลที่น่าสนใจในการจัดเก็บค่าชดเชยที่ไม่ใช่ศูนย์
  • ถือว่าเวลา UTC ที่ไม่ใช่ศูนย์ทั้งหมดเป็นข้อผิดพลาดในการป้อนข้อมูล
  • อย่าผสมและจับคู่การประทับเวลาแบบสัมพัทธ์และแบบสัมบูรณ์
  • ใช้UTCเป็นtimezoneฐานข้อมูลด้วยถ้าเป็นไปได้

หมายเหตุภาษาการเขียนโปรแกรมแบบสุ่ม: datetimeประเภทข้อมูลของ Python นั้นดีมากในการรักษาความแตกต่างระหว่างเวลาสัมบูรณ์เทียบกับเวลาสัมพัทธ์ (แม้ว่าจะน่าหงุดหงิดในตอนแรกจนกว่าคุณจะเสริมด้วยไลบรารีเช่นPyTZ )


แก้ไข

ให้ฉันอธิบายความแตกต่างระหว่างสัมพัทธ์กับสัมบูรณ์อีกเล็กน้อย

เวลาสัมบูรณ์ใช้ในการบันทึกเหตุการณ์ ตัวอย่าง: "ผู้ใช้ 123 เข้าสู่ระบบ" หรือ "พิธีจบการศึกษาเริ่มเวลา 2011-05-28 14.00 น. PST" ไม่ว่าเขตเวลาท้องถิ่นของคุณจะเป็นอย่างไรหากคุณสามารถเทเลพอร์ตไปยังที่ที่เกิดเหตุการณ์ขึ้นได้ ข้อมูลเวลาส่วนใหญ่ในฐานข้อมูลเป็นค่าสัมบูรณ์ (ดังนั้นควรTIMESTAMP WITH TIME ZONEเป็นค่าออฟเซ็ต +0 และป้ายข้อความที่แสดงถึงกฎที่ควบคุมเขตเวลาเฉพาะ - ไม่ใช่ค่าชดเชย)

เหตุการณ์สัมพัทธ์คือการบันทึกหรือกำหนดเวลาของบางสิ่งจากมุมมองของเขตเวลาที่ยังไม่ได้กำหนด ตัวอย่าง: "ประตูธุรกิจของเราเปิดเวลา 8.00 น. และปิดเวลา 21.00 น.", "นัดพบกันทุกวันจันทร์เวลา 7.00 น. สำหรับการประชุมอาหารเช้าประจำสัปดาห์" หรือ "ทุกวันฮาโลวีนเวลา 20.00 น." โดยทั่วไปเวลาสัมพัทธ์จะใช้ในเทมเพลตหรือโรงงานสำหรับเหตุการณ์และเวลาที่แน่นอนจะใช้สำหรับเกือบทุกอย่าง มีข้อยกเว้นที่หายากอย่างหนึ่งที่ควรค่าแก่การชี้ให้เห็นซึ่งควรแสดงให้เห็นถึงคุณค่าของเวลาสัมพัทธ์ สำหรับเหตุการณ์ในอนาคตที่ไกลพอในอนาคตซึ่งอาจมีความไม่แน่นอนเกี่ยวกับเวลาที่แน่นอนที่บางสิ่งอาจเกิดขึ้นให้ใช้การประทับเวลาแบบสัมพัทธ์ นี่คือตัวอย่างโลกแห่งความจริง:

สมมติว่าเป็นปี 2004 และคุณต้องกำหนดเวลาส่งมอบในวันที่ 31 ตุลาคม 2008 เวลา 13.00 น. ทางฝั่งตะวันตกของสหรัฐอเมริกา (เช่นAmerica/Los_Angeles/ PST8PDT) หากคุณเก็บข้อมูลนั้นโดยใช้เวลาสัมบูรณ์’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONEการจัดส่งจะปรากฏขึ้นในเวลา 14.00 น. เนื่องจากรัฐบาลสหรัฐฯผ่านพระราชบัญญัตินโยบายพลังงานปี 2548ที่เปลี่ยนแปลงกฎที่ควบคุมเวลาออมแสง ในปี 2004 เมื่อกำหนดการส่งมอบวันที่10-31-2008จะเป็นเวลามาตรฐานแปซิฟิก ( +8000) แต่เริ่มต้นในปี 2548 ฐานข้อมูลเขตเวลา + ที่ได้รับการยอมรับว่า10-31-2008จะเป็นเวลาออมแสงของแปซิฟิก (+0700) การจัดเก็บการประทับเวลาแบบสัมพัทธ์กับเขตเวลาจะส่งผลให้กำหนดการส่งมอบที่ถูกต้องเนื่องจากการประทับเวลาแบบสัมพัทธ์นั้นไม่สามารถป้องกันการปลอมแปลงโดยแจ้งให้รัฐสภาทราบได้ ในกรณีที่จุดตัดระหว่างการใช้เวลาสัมพัทธ์เทียบกับเวลาสัมบูรณ์สำหรับการกำหนดเวลาสิ่งต่างๆคือเส้นที่คลุมเครือ แต่กฎง่ายๆของฉันคือการตั้งเวลาสำหรับสิ่งใด ๆ ในอนาคตที่ไกลกว่า 3-6mo ควรใช้การประทับเวลาสัมพัทธ์ (กำหนดเวลา = สัมบูรณ์เทียบกับที่วางแผนไว้ = ญาติ ???).

เวลาสัมพัทธ์อื่น ๆ / ประเภทสุดท้ายคือINTERVAL. ตัวอย่าง: "เซสชันจะหมดเวลา 20 นาทีหลังจากผู้ใช้เข้าสู่ระบบ" INTERVALสามารถนำมาใช้อย่างถูกต้องด้วยการประทับเวลาสัมบูรณ์ ( TIMESTAMP WITH TIME ZONE) หรือ timestamps ญาติ ( TIMESTAMP WITHOUT TIME ZONE) เป็นเรื่องที่ถูกต้องพอ ๆ กันที่จะพูดว่า "เซสชันของผู้ใช้จะหมดอายุ 20 นาทีหลังจากการเข้าสู่ระบบสำเร็จ (login_utc + session_duration)" หรือ "การประชุมอาหารเช้าในตอนเช้าของเราสามารถใช้เวลาเพียง 60 นาทีเท่านั้น (ซ้ำซากจำเจ + เวลาการประชุม)"

บิตสุดท้ายของความสับสน: DATE, TIME, TIME WITHOUT TIME ZONEและTIME WITH TIME ZONEทุกประเภทข้อมูลญาติ ตัวอย่างเช่น: '2011-05-28'::DATEแสดงวันที่สัมพัทธ์เนื่องจากคุณไม่มีข้อมูลเขตเวลาซึ่งสามารถใช้ระบุเวลาเที่ยงคืนได้ ในทำนองเดียวกัน'23:23:59'::TIMEมีความสัมพันธ์กันเนื่องจากคุณไม่ทราบว่าเขตเวลาหรือเขตเวลาที่DATEแสดง แม้ว่า'23:59:59-07'::TIME WITH TIME ZONEคุณจะไม่รู้ว่าDATEจะเป็นอย่างไร และสุดท้ายDATEด้วยเขตเวลาไม่ได้เป็นจริงDATEมันคือTIMESTAMP WITH TIME ZONE:

test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
      timezone       
---------------------
 2011-05-11 07:00:00
(1 row)

test=> SET timezone = 'UTC';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
      timezone       
---------------------
 2011-05-11 00:00:00
(1 row)

การใส่วันที่และเขตเวลาในฐานข้อมูลเป็นสิ่งที่ดี แต่ก็เป็นเรื่องง่ายที่จะได้ผลลัพธ์ที่ไม่ถูกต้องเล็กน้อย ต้องใช้ความพยายามเพิ่มเติมเพียงเล็กน้อยในการจัดเก็บข้อมูลเวลาอย่างถูกต้องและสมบูรณ์ แต่นั่นไม่ได้หมายความว่าต้องใช้ความพยายามเพิ่มเติมเสมอไป


2
หากคุณบอก postgresql อย่างถูกต้องถึงเขตเวลาที่ถูกต้องว่าการประทับเวลาของผู้ใช้อยู่ใน postgresql จะดำเนินการอย่างหนักในเบื้องหลัง การแปลงตัวเองเป็นเพียงการยืมปัญหา
Seth Robertson

1
@ ฌอน - ด้วยข้อ จำกัด ในการตรวจสอบของคุณคุณจะแทรกการประทับเวลาโดยไม่ต้องทำset timezone to 'UTC'อย่างไร คุณรู้หรือไม่ว่าวันที่ที่ทราบเขตเวลาทั้งหมดจะถูกเก็บไว้ภายในใน UTC ?

2
จุดสำคัญของการตรวจสอบคือเพื่อให้แน่ใจว่าข้อมูลถูกจัดเก็บโดยมีศูนย์ชดเชยจาก UTC การเรียงลำดับและการดึงข้อมูลและการเปรียบเทียบเวลากับออฟเซ็ตที่ไม่ใช่ศูนย์นั้นเกิดข้อผิดพลาดได้ง่าย ด้วยการบังคับใช้ค่าชดเชย UTC เป็นศูนย์คุณสามารถโต้ตอบกับข้อมูลได้อย่างสม่ำเสมอจากมุมมองเดียวในลักษณะที่มีความเสี่ยงเกือบเป็นศูนย์ซึ่งทำงานได้อย่างคาดเดาได้ในทุกสถานการณ์ หากการประทับเวลาใช้งานได้จริงเพื่อสนับสนุนการแสดงเขตเวลาตามข้อความความคิดของฉันเกี่ยวกับเรื่องนี้ก็จะแตกต่างออกไป : ~]
ฌอน

6
@Sean: แต่อย่างที่แจ็คระบุการประทับเวลาที่ทราบเขตเวลาทั้งหมดโดยพื้นฐานจะถูกจัดเก็บไว้ภายในใน UTC และจะถูกแปลงเป็นเขตเวลาท้องถิ่นของคุณเมื่อใช้ อย่างมีประสิทธิภาพการแยก (เขตเวลาจาก ... ) จะส่งคืนทุกครั้งที่เขตเวลาท้องถิ่นของการเชื่อมต่อคือ: ไม่มีความสัมพันธ์กับการ "จัดเก็บ" การประทับเวลา กำหนดให้แตกต่างกันโซนเวลาไม่ได้เป็นส่วนหนึ่งของประเภทเลยและไม่สามารถจัดเก็บได้: "กับเขตเวลา" เป็นเพียงคุณสมบัติของวิธีที่ข้อมูลจะถูกแปลงเมื่อโต้ตอบกับประเภทอื่น ข้อมูลจึงไม่มีการแสดงเขตเวลาเลยทั้งแบบข้อความหรืออื่น ๆ
Jay Freeman -saurik-

@ JayFreeman-saurik-: คุณพูดถูกจริงๆ '' CHECK () '' เป็นมาตรการป้องกันการยิงด้วยเท้าเพื่อป้องกันรหัสที่อาจหลบได้ การตรวจสอบให้แน่ใจว่าข้อมูลเป็น UTC ในการเขียนเป็นการรับประกันเล็กน้อยว่าโค้ดถูกคิดผ่านหรือสภาพแวดล้อมการดำเนินการถูกตั้งค่าอย่างถูกต้อง
Sean

59

คำตอบของฌอนซับซ้อนเกินไปและทำให้เข้าใจผิด

ความจริงก็คือทั้ง "WITH TIME ZONE" และ "WITHOUT TIME ZONE" จะจัดเก็บค่าเป็นเวลา UTC สัมบูรณ์แบบ Unix เหมือนกัน ความแตกต่างทั้งหมดอยู่ที่วิธีการแสดงการประทับเวลา เมื่อ "มีเขตเวลา" ค่าที่แสดงจะเป็นค่าที่จัดเก็บ UTC ที่แปลเป็นโซนของผู้ใช้ เมื่อ "ไม่มีเขตเวลา" ค่า UTC ที่เก็บไว้จะถูกบิดเพื่อให้แสดงหน้าปัดนาฬิกาเดียวกันไม่ว่าผู้ใช้จะตั้งค่าโซนใดก็ตาม "

สถานการณ์เดียวที่ "ไม่มีเขตเวลา" สามารถใช้งานได้คือเมื่อใช้ค่าหน้าปัดนาฬิกาโดยไม่คำนึงถึงโซนจริง ตัวอย่างเช่นเมื่อการประทับเวลาระบุเวลาที่คูหาลงคะแนนอาจปิด (เช่นปิดเวลา 20:00 น. โดยไม่คำนึงถึงเขตเวลาของบุคคล)

ใช้ตัวเลือก 3 ใช้ "กับเขตเวลา" เสมอเว้นแต่จะมีเหตุผลที่เฉพาะเจาะจงมาก


10
David E. Wheeler ผู้เชี่ยวชาญ Postgres รายใหญ่จะเห็นด้วยกับการประเมินของคุณตามโพสต์ของเขาใช้ TIMESTAMP กับ TIME ZONEเสมอ
Basil Bourque

2
จะเป็นอย่างไรหากคุณจะให้เบราว์เซอร์แปลงการประทับเวลา UTC เป็นเขตเวลาท้องถิ่น ดังนั้น db จะไม่ทำการแปลงและมีเฉพาะ UTC เท่านั้น "ไม่มีเขตเวลา" เป็นที่ยอมรับหรือไม่
dman

5

ความชอบของฉันอยู่ที่ตัวเลือกที่ 3 เนื่องจาก Postgres สามารถคำนวณการประทับเวลาใหม่โดยเทียบกับเขตเวลาสำหรับคุณในขณะที่อีกสองรายการคุณจะต้องทำด้วยตัวเอง ค่าใช้จ่ายในการจัดเก็บข้อมูลเพิ่มเติมในการจัดเก็บการประทับเวลาด้วยเขตเวลานั้นมีความสำคัญน้อยมากเว้นแต่คุณจะพูดถึงบันทึกหลายล้านรายการซึ่งในกรณีนี้คุณอาจมีข้อกำหนดในการจัดเก็บข้อมูลที่ค่อนข้างมีเนื้อสัตว์อยู่แล้ว


19
ไม่ถูกต้อง ไม่มีค่าใช้จ่าย… Postgres ไม่จัดเก็บเขตเวลา ('offset' เป็นคำที่ถูกต้องไม่ใช่เขตเวลาโดยวิธีการ) TIMESTAMP WITH TIME ZONEชื่อเป็นความเข้าใจผิด หมายความว่า "ใส่ใจกับค่าชดเชยที่ระบุเมื่อแทรก / อัปเดตและใช้ค่าชดเชยนั้นเพื่อปรับวันที่ - เวลาเป็น UTC" TIMESTAMP WITHOUT TIME ZONEชื่อหมายถึง "ไม่สนใจใด ๆ ชดเชยที่อาจจะอยู่ในช่วงแทรก / อัปเดตให้พิจารณาวันที่และเวลาส่วนที่เป็นเวลา UTC กับความจำเป็นในการปรับตัวไม่" อ่านเอกสารอย่างละเอียด
Basil Bourque

1
@BasilBourque ขอบคุณสำหรับข้อมูลนี้ มีประโยชน์อย่างไม่น่าเชื่อ สำหรับผู้อื่นที่อ่านข้อความนี้บรรทัดจากเอกสารกล่าวว่า "ในลิเทอรัลที่ถูกกำหนดให้ประทับเวลาโดยไม่มีไทม์โซน PostgreSQL จะเพิกเฉยต่อการบ่งชี้โซนเวลาใด ๆ นั่นคือค่าผลลัพธ์จะได้มาจากฟิลด์วันที่ / เวลาใน ค่าอินพุตและไม่ได้รับการปรับสำหรับโซนเวลา "
Aidan Rosswood
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.