ฉันได้แก้ไขปัญหานี้แล้วโดยการมีตารางปฏิทินที่ง่ายมาก - ในแต่ละปีจะมีหนึ่งแถวต่อเขตเวลาที่สนับสนุนโดยมีออฟเซ็ตมาตรฐานและช่วงเวลาเริ่มต้นและวันที่สิ้นสุดของ DST และออฟเซ็ต (ถ้าเขตเวลานั้นรองรับ) จากนั้นฟังก์ชั่นที่มีมูลค่าเป็นตารางแบบอินไลน์ฟังก์ชันที่มีค่าเป็นตารางซึ่งต้องใช้เวลาแหล่งที่มา (ใน UTC แน่นอน) และเพิ่ม / ลบออฟเซ็ต
เห็นได้ชัดว่าสิ่งนี้จะไม่ทำงานได้ดีมากหากคุณรายงานข้อมูลส่วนใหญ่ การแบ่งพาร์ติชันอาจช่วยได้ แต่คุณยังคงมีกรณีที่สองสามชั่วโมงสุดท้ายในหนึ่งปีหรือสองสามชั่วโมงแรกในปีถัดไปเป็นจริงของปีที่แตกต่างกันเมื่อแปลงเป็นเขตเวลาที่เฉพาะเจาะจง - ดังนั้นคุณจึงไม่เคยได้รับพาร์ติชันที่แท้จริง การแยกยกเว้นเมื่อช่วงการรายงานของคุณไม่รวมวันที่ 31 ธันวาคมหรือ 1 มกราคม
มีสองสามกรณีขอบแปลก ๆ ที่คุณต้องพิจารณา:
2014-11-02 05:30 UTC และ 2014-11-02 06:30 UTC ทั้งคู่เปลี่ยนเป็น 01:30 น. ในเขตเวลาตะวันออกตัวอย่างเช่น (ครั้งแรกเป็นครั้งแรกที่ 01:30 ถูกชนในพื้นที่จากนั้นหนึ่งครั้ง เป็นครั้งที่สองเมื่อนาฬิกาย้อนกลับจาก 2:00 น. ถึง 1:00 น. และผ่านไปอีกครึ่งชั่วโมง) ดังนั้นคุณต้องตัดสินใจว่าจะจัดการกับชั่วโมงของการรายงานได้อย่างไรตาม UTC คุณควรเห็นปริมาณการใช้งานหรือปริมาณของสิ่งที่คุณวัดเป็นสองเท่าเมื่อสองชั่วโมงนั้นถูกแมปกับหนึ่งชั่วโมงในเขตเวลาที่สังเกต DST สิ่งนี้สามารถเล่นเกมสนุก ๆ ที่มีการจัดลำดับเหตุการณ์เนื่องจากบางสิ่งที่ต้องเกิดขึ้นอย่างมีเหตุมีผลหลังจากที่บางอย่างปรากฏขึ้นที่จะเกิดขึ้นก่อนหน้านี้เมื่อเวลาถูกปรับเป็นชั่วโมงเดียวแทนสอง ตัวอย่างสุดขั้วคือการดูหน้าเว็บที่เกิดขึ้นในเวลา 05:59 UTC จากนั้นคลิกที่เกิดขึ้นเวลา 06:00 UTC ในเวลา UTC สิ่งเหล่านี้เกิดขึ้นหนึ่งนาที แต่เมื่อแปลงเป็นเวลาตะวันออกมุมมองเกิดขึ้นเวลา 1:59 น. และการคลิกเกิดขึ้นหนึ่งชั่วโมงก่อนหน้านี้
2014-03-09 02:30 ไม่เคยเกิดขึ้นในสหรัฐอเมริกา นี่เป็นเพราะเวลา 2:00 น. เราจะหมุนนาฬิกาไปข้างหน้าจนถึง 3:00 น มีโอกาสมากที่คุณจะต้องการแจ้งข้อผิดพลาดหากผู้ใช้ป้อนเวลาดังกล่าวและขอให้คุณแปลงเป็น UTC หรือออกแบบฟอร์มของคุณเพื่อให้ผู้ใช้ไม่สามารถเลือกเวลาดังกล่าวได้
แม้ว่าในกรณีเหล่านั้นในใจฉันยังคิดว่าคุณมีวิธีการที่ถูกต้อง: เก็บข้อมูลใน UTC การแมปข้อมูลไปยังโซนเวลาอื่น ๆ จาก UTC ได้ง่ายกว่าจากบางโซนเวลาไปยังโซนเวลาอื่นโดยเฉพาะอย่างยิ่งเมื่อเขตเวลาเริ่มต้น / สิ้นสุด DST แตกต่างกันในวันที่ที่แตกต่างกันและแม้แต่เขตเวลาเดียวกันสามารถสลับการใช้กฎที่แตกต่างกัน เช่นสหรัฐอเมริกาเปลี่ยนกฎ 6 ปีก่อนหรือมากกว่านั้น)
คุณจะต้องการใช้ตารางปฏิทินสำหรับสิ่งเหล่านี้ไม่ใช่CASE
นิพจน์ที่ใหญ่โต(ไม่ใช่คำสั่ง ) ฉันเพิ่งเขียนชุดสามส่วนสำหรับMSSQLTips.comเกี่ยวกับเรื่องนี้; ฉันคิดว่าส่วนที่ 3 จะมีประโยชน์มากที่สุดสำหรับคุณ:
http://www.mssqltips.com/sqlservertip/3173/handle-conversion-between-time-zones-in-sql-server--part-1/
http://www.mssqltips.com/sqlservertip/3174/handle-conversion-between-time-zones-in-sql-server--part-2/
http://www.mssqltips.com/sqlservertip/3175/handle-conversion-between-time-zones-in-sql-server--part-3/
ตัวอย่างจริงในขณะเดียวกัน
สมมติว่าคุณมีตารางความจริงง่ายๆ ข้อเท็จจริงเดียวที่ฉันสนใจในกรณีนี้คือเวลาของเหตุการณ์ แต่ฉันจะเพิ่ม GUID ที่ไม่มีความหมายเพียงเพื่อทำให้ตารางกว้างพอที่จะสนใจ อีกครั้งเพื่อให้ชัดเจนตารางข้อเท็จจริงเก็บเหตุการณ์ในเวลา UTC และเวลา UTC เท่านั้น ฉันได้ต่อท้ายคอลัมน์ด้วย_UTC
ดังนั้นจึงไม่มีความสับสน
CREATE TABLE dbo.Fact
(
EventTime_UTC DATETIME NOT NULL,
Filler UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID()
);
GO
CREATE CLUSTERED INDEX x ON dbo.Fact(EventTime_UTC);
GO
ตอนนี้เรามาโหลดตารางข้อเท็จจริงของเรากับ 10,000,000 แถว - แทนทุก 3 วินาที (1,200 แถวต่อชั่วโมง) จาก 2013-12-30 เวลาเที่ยงคืน UTC จนกระทั่งบางครั้งหลังจาก 5:00 UTC ใน 2014-12-12 สิ่งนี้ช่วยให้มั่นใจได้ว่าข้อมูลจะเลาะเลียบไปตามขอบเขตปีเช่นเดียวกับ DST ไปข้างหน้าและย้อนกลับสำหรับเขตเวลาหลายเขต มันดูน่ากลัวจริงๆ แต่ใช้เวลาประมาณ 9 วินาทีในระบบของฉัน ตารางควรอยู่ที่ประมาณ 325 MB
;WITH x(c) AS
(
SELECT TOP (10000000) DATEADD(SECOND,
3*(ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1),
'20131230')
FROM sys.all_columns AS s1
CROSS JOIN sys.all_columns AS s2
ORDER BY s1.[object_id]
)
INSERT dbo.Fact WITH (TABLOCKX) (EventTime_UTC)
SELECT c FROM x;
และเพื่อแสดงให้เห็นว่าเคียวรีค้นหาทั่วไปจะมีลักษณะเป็นอย่างไรกับตารางแถว 10 มม. นี้หากฉันเรียกใช้คิวรีนี้:
SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0),
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= '20140308'
AND EventTime_UTC < '20140311'
GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0);
ฉันได้รับแผนนี้และผลตอบแทนเป็น 25 มิลลิวินาที * อ่าน 358 ครั้งเพื่อส่งคืนผลรวม 72 ชั่วโมง:
* ระยะเวลาที่วัดโดยSQL Sentry Plan Explorerฟรีของเราซึ่งไม่ใช้ผลลัพธ์ดังนั้นนี่จึงไม่รวมเวลาการถ่ายโอนข้อมูลเครือข่ายของการเรนเดอร์ ฯลฯ เป็นข้อจำกัดความรับผิดชอบเพิ่มเติมฉันทำงานกับ SQL Sentry
เห็นได้ชัดว่าใช้เวลานานขึ้นเล็กน้อยถ้าฉันทำให้ช่วงของฉันใหญ่เกินไป - หนึ่งเดือนมีข้อมูล 258ms สองเดือนใช้เวลามากกว่า 500 มิลลิวินาทีและอื่น ๆ คู่ขนานอาจเตะใน:
นี่คือที่ที่คุณเริ่มคิดเกี่ยวกับวิธีการแก้ปัญหาอื่น ๆ ที่ดีกว่าเพื่อตอบสนองคำสั่งการรายงานและไม่มีอะไรเกี่ยวข้องกับเขตเวลาที่เอาต์พุตของคุณจะแสดง ฉันจะไม่เข้าไปในนั้นฉันแค่ต้องการแสดงให้เห็นว่าการแปลงเขตเวลาไม่ได้ทำให้แบบสอบถามการรายงานของคุณดูดมากขึ้นไปอีกและพวกเขาอาจดูดถ้าคุณได้รับช่วงขนาดใหญ่ที่ไม่ได้รับการสนับสนุนที่เหมาะสม ดัชนี ฉันจะใช้ช่วงวันที่ขนาดเล็กเพื่อแสดงว่าตรรกะนั้นถูกต้องและให้คุณกังวลเกี่ยวกับการทำให้แน่ใจว่าแบบสอบถามแบบรายงานตามช่วงของคุณทำงานได้อย่างเพียงพอโดยมีหรือไม่มีการแปลงเขตเวลา
โอเคตอนนี้เราต้องใช้ตารางเพื่อจัดเก็บเขตเวลาของเรา (ด้วยออฟเซ็ตในหน่วยนาทีเนื่องจากทุกคนไม่ได้ปิด UTC) และ DST เปลี่ยนวันที่ที่รองรับในแต่ละปี เพื่อความง่ายฉันจะป้อนเขตเวลาเพียงไม่กี่ปีและหนึ่งปีเพื่อจับคู่ข้อมูลด้านบน
CREATE TABLE dbo.TimeZones
(
TimeZoneID TINYINT NOT NULL PRIMARY KEY,
Name VARCHAR(9) NOT NULL,
Offset SMALLINT NOT NULL, -- minutes
DSTName VARCHAR(9) NOT NULL,
DSTOffset SMALLINT NOT NULL -- minutes
);
รวมเขตเวลาสำหรับความหลากหลายด้วยบางส่วนมีออฟเซ็ตครึ่งชั่วโมงบางตัวไม่เห็น DST โปรดทราบว่าออสเตรเลียในซีกโลกใต้สังเกต DST ในช่วงฤดูหนาวของเราดังนั้นนาฬิกาของพวกเขาจะกลับไปในเดือนเมษายนและส่งต่อในเดือนตุลาคม (ตารางด้านบนพลิกชื่อ แต่ฉันไม่แน่ใจว่าจะทำให้โซนเวลาซีกโลกใต้สับสนน้อยลงได้อย่างไร)
INSERT dbo.TimeZones VALUES
(1, 'UTC', 0, 'UTC', 0),
(2, 'GMT', 0, 'BST', 60),
-- London = UTC in winter, +1 in summer
(3, 'EST', -300, 'EDT', -240),
-- East coast US (-5 h in winter, -4 in summer)
(4, 'ACDT', 630, 'ACST', 570),
-- Adelaide (Australia) +10.5 h Oct - Apr, +9.5 Apr - Oct
(5, 'ACST', 570, 'ACST', 570);
-- Darwin (Australia) +9.5 h year round
ทีนี้ตารางปฏิทินที่ต้องรู้เมื่อมีการเปลี่ยนแปลง TZ ฉันจะแทรกแถวของความสนใจ (แต่ละเขตเวลาด้านบนและเฉพาะการเปลี่ยนแปลงเวลาสำหรับปี 2014) เพื่อความสะดวกในการคำนวณกลับไปกลับมาฉันเก็บทั้งช่วงเวลาใน UTC ที่มีการเปลี่ยนแปลงเขตเวลาและช่วงเวลาเดียวกันในเวลาท้องถิ่น สำหรับเขตเวลาที่ไม่สังเกต DST เป็นมาตรฐานตลอดทั้งปีและ DST "เริ่มต้น" ในวันที่ 1 มกราคม
CREATE TABLE dbo.Calendar
(
TimeZoneID TINYINT NOT NULL FOREIGN KEY
REFERENCES dbo.TimeZones(TimeZoneID),
[Year] SMALLDATETIME NOT NULL,
UTCDSTStart SMALLDATETIME NOT NULL,
UTCDSTEnd SMALLDATETIME NOT NULL,
LocalDSTStart SMALLDATETIME NOT NULL,
LocalDSTEnd SMALLDATETIME NOT NULL,
PRIMARY KEY (TimeZoneID, [Year])
);
คุณสามารถเติมสิ่งนี้ด้วยอัลกอริธึม (และชุดเคล็ดลับที่กำลังจะมาถึงใช้เทคนิคการตั้งค่าที่ฉลาดถ้าฉันบอกตัวเอง) แทนที่จะวนซ้ำเติมด้วยตนเองคุณมีอะไรบ้าง สำหรับคำตอบนี้ฉันตัดสินใจที่จะใส่ข้อมูลด้วยตนเองเป็นเวลาหนึ่งปีสำหรับเขตเวลาห้าโซนและฉันจะไม่ไปสนใจกลอุบายแฟนซีใด ๆ
INSERT dbo.Calendar VALUES
(1, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00'),
(2, '20140101', '20140330 01:00','20141026 00:00','20140330 02:00','20141026 01:00'),
(3, '20140101', '20140309 07:00','20141102 06:00','20140309 03:00','20141102 01:00'),
(4, '20140101', '20140405 16:30','20141004 16:30','20140406 03:00','20141005 02:00'),
(5, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00');
โอเคเรามีข้อมูลข้อเท็จจริงของเราและตาราง "มิติ" ของเรา (ฉันประจบประแจงเมื่อฉันพูดอย่างนั้น) แล้วตรรกะคืออะไร ฉันคิดว่าคุณจะต้องให้ผู้ใช้เลือกเขตเวลาและป้อนช่วงวันที่ของแบบสอบถาม ฉันจะสมมติว่าช่วงวันที่จะเป็นวันเต็มในเขตเวลาของตนเอง ไม่มีวันบางส่วนไม่ต้องสนใจชั่วโมงบางส่วน ดังนั้นพวกเขาจะผ่านในวันที่เริ่มต้นวันที่สิ้นสุดและ TimeZoneID จากนั้นเราจะใช้ฟังก์ชันสเกลาร์เพื่อแปลงวันที่เริ่มต้น / สิ้นสุดจากเขตเวลานั้นเป็น UTC ซึ่งจะช่วยให้เราสามารถกรองข้อมูลตามช่วง UTC เมื่อเราดำเนินการแล้วและทำการรวมกับมันแล้วเราสามารถนำการแปลงของเวลาที่จัดกลุ่มกลับไปยังโซนเวลาต้นทางก่อนที่จะแสดงต่อผู้ใช้
สเกลาร์ UDF:
CREATE FUNCTION dbo.ConvertToUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS SMALLDATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN
(
SELECT DATEADD(MINUTE, -CASE
WHEN @Source >= src.LocalDSTStart
AND @Source < src.LocalDSTEnd THEN t.DSTOffset
WHEN @Source >= DATEADD(HOUR,-1,src.LocalDSTStart)
AND @Source < src.LocalDSTStart THEN NULL
ELSE t.Offset END, @Source)
FROM dbo.Calendar AS src
INNER JOIN dbo.TimeZones AS t
ON src.TimeZoneID = t.TimeZoneID
WHERE src.TimeZoneID = @SourceTZ
AND t.TimeZoneID = @SourceTZ
AND DATEADD(MINUTE,t.Offset,@Source) >= src.[Year]
AND DATEADD(MINUTE,t.Offset,@Source) < DATEADD(YEAR, 1, src.[Year])
);
END
GO
และฟังก์ชั่นที่มีค่าเป็นตาราง:
CREATE FUNCTION dbo.ConvertFromUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT
[Target] = DATEADD(MINUTE, CASE
WHEN @Source >= trg.UTCDSTStart
AND @Source < trg.UTCDSTEnd THEN tz.DSTOffset
ELSE tz.Offset END, @Source)
FROM dbo.Calendar AS trg
INNER JOIN dbo.TimeZones AS tz
ON trg.TimeZoneID = tz.TimeZoneID
WHERE trg.TimeZoneID = @SourceTZ
AND tz.TimeZoneID = @SourceTZ
AND @Source >= trg.[Year]
AND @Source < DATEADD(YEAR, 1, trg.[Year])
);
และกระบวนการที่ใช้ ( แก้ไข : อัปเดตเพื่อจัดการการจัดกลุ่มออฟเซ็ต 30 นาที):
CREATE PROCEDURE dbo.ReportOnDateRange
@Start SMALLDATETIME, -- whole dates only please!
@End SMALLDATETIME, -- whole dates only please!
@TimeZoneID TINYINT
AS
BEGIN
SET NOCOUNT ON;
SELECT @Start = dbo.ConvertToUTC(@Start, @TimeZoneID),
@End = dbo.ConvertToUTC(@End, @TimeZoneID);
;WITH x(t,c) AS
(
SELECT DATEDIFF(MINUTE, @Start, EventTime_UTC)/60,
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= @Start
AND EventTime_UTC < DATEADD(DAY, 1, @End)
GROUP BY DATEDIFF(MINUTE, @Start, EventTime_UTC)/60
)
SELECT
UTC = DATEADD(MINUTE, x.t*60, @Start),
[Local] = y.[Target],
[RowCount] = x.c
FROM x OUTER APPLY
dbo.ConvertFromUTC(DATEADD(MINUTE, x.t*60, @Start), @TimeZoneID) AS y
ORDER BY UTC;
END
GO
(คุณอาจต้องการไปที่ลัดวงจรที่นั่นหรือขั้นตอนการจัดเก็บแยกต่างหากในกรณีที่ผู้ใช้ต้องการรายงานใน UTC - การแปลไปและกลับจาก UTC เป็นงานที่ยุ่งมากอย่างเห็นได้ชัด)
ตัวอย่างการโทร:
EXEC dbo.ReportOnDateRange
@Start = '20140308',
@End = '20140311',
@TimeZoneID = 3;
ส่งคืนใน 41ms * และสร้างแผนนี้:
* อีกครั้งพร้อมผลลัพธ์ที่ถูกทิ้ง
สำหรับ 2 เดือนจะส่งคืนใน 507ms และแผนเหมือนกันนอกเหนือจาก rowcounts:
ในขณะที่มีความซับซ้อนมากขึ้นเล็กน้อยและใช้เวลาเพิ่มขึ้นเล็กน้อยฉันค่อนข้างมั่นใจว่าวิธีการแบบนี้จะทำงานได้ดีกว่าวิธีการเชื่อมโยงตาราง และนี่เป็นตัวอย่างข้อมือปิดสำหรับคำตอบ dba.se; ฉันแน่ใจว่าตรรกะและประสิทธิภาพของฉันสามารถปรับปรุงได้โดยคนฉลาดกว่าฉันมาก
คุณสามารถอ่านข้อมูลเพื่อดูกรณีขอบที่ฉันพูดถึง - ไม่มีแถวเอาท์พุทสำหรับชั่วโมงที่นาฬิกาหมุนไปข้างหน้าสองแถวสำหรับชั่วโมงที่พวกเขาย้อนกลับ (และชั่วโมงนั้นเกิดขึ้นสองครั้ง) นอกจากนี้คุณยังสามารถเล่นกับค่าที่ไม่ดี; ถ้าคุณผ่านในปี 20140309 02:30 เวลาฝั่งตะวันออกมันจะไม่ทำงานได้ดีเกินไป
ฉันอาจไม่มีสมมติฐานทั้งหมดที่ถูกต้องเกี่ยวกับวิธีการรายงานของคุณจะทำงานดังนั้นคุณอาจต้องทำการปรับเปลี่ยนบางอย่าง แต่ฉันคิดว่านี่ครอบคลุมพื้นฐาน