Tôi đã giải quyết điều này bằng cách có một bảng lịch rất đơn giản - mỗi năm có một hàng cho mỗi múi giờ được hỗ trợ , với độ lệch chuẩn và mốc thời gian bắt đầu / thời gian kết thúc của DST và độ lệch của nó (nếu múi giờ đó hỗ trợ nó). Sau đó, một hàm nội tuyến, ràng buộc lược đồ, có giá trị bảng sẽ lấy thời gian nguồn (tất nhiên là trong UTC) và cộng / trừ phần bù.
Điều này rõ ràng sẽ không bao giờ thực hiện cực kỳ tốt nếu bạn đang báo cáo đối với một phần lớn dữ liệu; phân vùng có vẻ hữu ích, nhưng bạn vẫn sẽ gặp trường hợp vài giờ cuối cùng trong một năm hoặc vài giờ đầu tiên trong năm tiếp theo thực sự thuộc về một năm khác khi được chuyển đổi sang múi giờ cụ thể - vì vậy bạn không bao giờ có thể có được phân vùng thực sự cách ly, ngoại trừ khi phạm vi báo cáo của bạn không bao gồm ngày 31 tháng 12 hoặc ngày 1 tháng 1.
Có một vài trường hợp cạnh kỳ lạ bạn cần xem xét:
2014/11/02 05:30 UTC và 2014-11 / 02 06:30 UTC cả hai chuyển đổi thành 01:30 AM theo múi giờ miền Đông, ví dụ (một lần đầu tiên 01:30 bị tấn công cục bộ, và sau đó một lần lần thứ hai khi đồng hồ quay trở lại từ 2:00 sáng đến 1:00 sáng và nửa giờ nữa trôi qua). Vì vậy, bạn cần quyết định cách xử lý giờ báo cáo đó - theo UTC, bạn sẽ thấy gấp đôi lưu lượng hoặc khối lượng của bất cứ thứ gì bạn đo được khi hai giờ đó được ánh xạ thành một giờ trong múi giờ quan sát DST. Điều này cũng có thể chơi các trò chơi vui nhộn với chuỗi các sự kiện, vì điều gì đó đã xảy ra một cách hợp lý sau khi một cái gì đó khác có thể xuất hiệnxảy ra trước khi nó được điều chỉnh thời gian thành một giờ thay vì hai giờ. Một ví dụ cực đoan là một lượt xem trang xảy ra lúc 05:59 UTC, sau đó một lần nhấp xảy ra lúc 06:00 UTC. Trong thời gian UTC, những điều này xảy ra cách nhau một phút, nhưng khi được chuyển đổi sang giờ phương Đông, chế độ xem xảy ra lúc 1:59 sáng và nhấp chuột đã xảy ra trước đó một giờ.
2014 / 03-09 02:30 không bao giờ xảy ra ở Hoa Kỳ. Điều này là do vào lúc 2:00 sáng, chúng tôi cuộn đồng hồ về phía trước đến 3:00 sáng. Vì vậy, có khả năng bạn sẽ muốn đưa ra lỗi nếu người dùng nhập thời gian như vậy và yêu cầu bạn chuyển đổi nó thành UTC hoặc thiết kế biểu mẫu của bạn để người dùng không thể chọn thời gian như vậy.
Ngay cả với những trường hợp cạnh đó, tôi vẫn nghĩ bạn có cách tiếp cận đúng: lưu trữ dữ liệu trong UTC. Việc ánh xạ dữ liệu sang các múi giờ khác từ UTC dễ dàng hơn nhiều so với từ múi giờ này sang múi giờ khác, đặc biệt là khi các múi giờ khác nhau bắt đầu / kết thúc DST vào các ngày khác nhau và thậm chí cùng múi giờ có thể chuyển đổi sử dụng các quy tắc khác nhau trong các năm khác nhau ( ví dụ Hoa Kỳ đã thay đổi các quy tắc 6 năm trước hoặc lâu hơn).
Bạn sẽ muốn sử dụng một bảng lịch cho tất cả những điều này, không phải là một CASE
biểu thức khổng lồ (không phải câu lệnh ). Tôi vừa viết một loạt ba phần cho MSSQLTips.com về điều này; Tôi nghĩ phần thứ 3 sẽ hữu ích nhất cho bạn:
http://www.mssqltips.com/sqlservertip/3173/handle-conversion-b between-time-zones-in-sql-server-part-1 /
http://www.mssqltips.com/sqlservertip/3174/handle-conversion-b between-time-zones-in-sql-server-part-2 /
http://www.mssqltips.com/sqlservertip/3175/handle-conversion-b between-time-zones-in-sql-server-part-3 /
Một ví dụ sống thực tế, trong khi đó
Giả sử bạn có một bảng thực tế rất đơn giản. Sự thật duy nhất tôi quan tâm trong trường hợp này là thời gian sự kiện, nhưng tôi sẽ thêm một GUID vô nghĩa chỉ để làm cho bàn đủ rộng để quan tâm. Một lần nữa, để rõ ràng, bảng thực tế chỉ lưu trữ các sự kiện trong thời gian UTC và thời gian UTC. Tôi thậm chí đã gắn cột với _UTC
vì vậy không có sự nhầm lẫn.
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
Bây giờ, hãy tải bảng thực tế của chúng tôi với 10.000.000 hàng - đại diện cho mỗi 3 giây (1.200 hàng mỗi giờ) từ 2013-12-30 vào lúc nửa đêm UTC cho đến khoảng sau 5 giờ sáng UTC ngày 2014-12-12. Điều này đảm bảo rằng dữ liệu nằm trong ranh giới một năm, cũng như DST chuyển tiếp và quay lại cho nhiều múi giờ. Điều này trông thực sự đáng sợ, nhưng mất ~ 9 giây trên hệ thống của tôi. Bảng sẽ kết thúc khoảng 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;
Và chỉ để hiển thị một truy vấn tìm kiếm thông thường sẽ trông như thế nào đối với bảng hàng 10MM này, nếu tôi chạy truy vấn này:
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);
Tôi nhận được kế hoạch này và nó sẽ trả về sau 25 mili giây *, thực hiện 358 lần đọc, để trả về tổng số 72 giờ:
* Thời lượng được đo bằng SQL Sentry Plan Explorer miễn phí của chúng tôi , loại bỏ kết quả, do đó, điều này không bao gồm thời gian chuyển mạng của dữ liệu, kết xuất, v.v. Là một từ chối trách nhiệm bổ sung, tôi làm việc cho SQL Sentry.
Rõ ràng là sẽ mất nhiều thời gian hơn một chút, nếu tôi làm cho phạm vi của mình quá lớn - một tháng dữ liệu mất tới 258ms, hai tháng mất hơn 500ms, v.v. Song song có thể đá trong:
Đây là nơi bạn bắt đầu nghĩ về các giải pháp khác, tốt hơn để đáp ứng các truy vấn báo cáo và nó không liên quan gì đến múi giờ mà đầu ra của bạn sẽ hiển thị. Tôi sẽ không hiểu điều đó, tôi chỉ muốn chứng minh rằng chuyển đổi múi giờ sẽ không thực sự khiến các truy vấn báo cáo của bạn bị thu hút nhiều hơn nữa và chúng có thể đã bị hút nếu bạn nhận được phạm vi lớn không được hỗ trợ bởi chính xác chỉ số. Tôi sẽ tuân theo các phạm vi ngày nhỏ để chỉ ra rằng logic là chính xác và để bạn lo lắng về việc đảm bảo các truy vấn báo cáo dựa trên phạm vi của bạn thực hiện đầy đủ, có hoặc không có chuyển đổi múi giờ.
Được rồi, bây giờ chúng tôi cần các bảng để lưu trữ múi giờ của chúng tôi (với độ lệch, tính bằng phút, vì không phải ai cũng nghỉ giờ UTC) và ngày thay đổi DST cho mỗi năm được hỗ trợ. Để đơn giản, tôi sẽ chỉ nhập một vài múi giờ và một năm để khớp với dữ liệu ở trên.
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
);
Bao gồm một vài múi giờ cho sự đa dạng, một số có thời gian giảm giá nửa giờ, một số không tuân thủ DST. Lưu ý rằng Úc, ở Nam bán cầu quan sát DST trong mùa đông của chúng tôi, vì vậy đồng hồ của họ quay trở lại vào tháng Tư và chuyển tiếp vào tháng Mười. (Bảng trên lật tên, nhưng tôi không chắc làm thế nào để làm cho điều này bớt khó hiểu hơn cho các múi giờ ở bán cầu nam.)
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
Bây giờ, một bảng lịch để biết khi nào TZ thay đổi. Tôi sẽ chỉ chèn các hàng quan tâm (mỗi múi giờ ở trên và chỉ thay đổi DST cho năm 2014). Để dễ dàng tính toán qua lại, tôi lưu trữ cả thời điểm trong UTC nơi múi giờ thay đổi và cùng thời điểm theo giờ địa phương. Đối với các múi giờ không tuân thủ DST, đó là tiêu chuẩn cả năm và DST "bắt đầu" vào ngày 1 tháng 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])
);
Bạn chắc chắn có thể tạo ra thuật toán này (và loạt mẹo sắp tới sử dụng một số kỹ thuật dựa trên tập hợp thông minh, nếu tôi tự nói như vậy), thay vì lặp, điền thủ công, bạn có gì. Đối với câu trả lời này, tôi quyết định chỉ nhập thủ công một năm cho năm múi giờ và tôi sẽ không bận tâm đến bất kỳ thủ thuật ưa thích nào.
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');
Được rồi, vì vậy chúng tôi có dữ liệu thực tế của chúng tôi và các bảng "thứ nguyên" của chúng tôi (tôi co rúm lại khi tôi nói điều đó), vậy logic là gì? Chà, tôi cho rằng bạn sẽ có người dùng chọn múi giờ của họ và nhập phạm vi ngày cho truy vấn. Tôi cũng sẽ cho rằng phạm vi ngày sẽ là ngày đầy đủ trong múi giờ riêng của họ; không một phần ngày, không bao giờ quan tâm một phần giờ. Vì vậy, họ sẽ vượt qua trong một ngày bắt đầu, ngày kết thúc và TimeZoneID. Từ đó, chúng tôi sẽ sử dụng hàm vô hướng để chuyển đổi ngày bắt đầu / ngày kết thúc từ múi giờ đó sang UTC, cho phép chúng tôi lọc dữ liệu dựa trên phạm vi UTC. Khi chúng tôi đã thực hiện điều đó và thực hiện các tổng hợp của mình trên đó, chúng tôi có thể áp dụng chuyển đổi thời gian được nhóm lại thành múi giờ nguồn trước khi hiển thị cho người dùng.
UDF vô hướng:
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
Và hàm có giá trị bảng:
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])
);
Và một quy trình sử dụng nó ( chỉnh sửa : cập nhật để xử lý nhóm bù 30 phút):
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
(Bạn có thể muốn thực hiện ngắn mạch ở đó hoặc một quy trình được lưu trữ riêng biệt, trong trường hợp người dùng muốn báo cáo trong UTC - rõ ràng việc dịch sang và từ UTC sẽ lãng phí công việc bận rộn.)
Cuộc gọi mẫu:
EXEC dbo.ReportOnDateRange
@Start = '20140308',
@End = '20140311',
@TimeZoneID = 3;
Trả về trong 41ms * và tạo gói này:
* Một lần nữa, với kết quả bị loại bỏ.
Trong 2 tháng, nó trả về sau 507ms và kế hoạch giống hệt với số lượng hàng:
Mặc dù phức tạp hơn một chút và tăng thời gian chạy một chút, tôi khá tự tin rằng cách tiếp cận này sẽ hiệu quả hơn nhiều, tốt hơn nhiều so với cách tiếp cận bảng cầu. Và đây là một ví dụ điển hình cho câu trả lời của dba.se; Tôi chắc rằng logic và hiệu quả của tôi có thể được cải thiện bởi những người thông minh hơn tôi nhiều.
Bạn có thể kiểm tra dữ liệu để xem các trường hợp cạnh mà tôi nói đến - không có hàng đầu ra nào cho giờ mà đồng hồ quay về phía trước, hai hàng cho giờ mà chúng quay ngược lại (và giờ đó xảy ra hai lần). Bạn cũng có thể chơi với các giá trị xấu; nếu bạn vượt qua vào năm 20140309 02:30 theo giờ phương Đông, chẳng hạn, nó sẽ không hoạt động tốt lắm.
Tôi có thể không có tất cả các giả định về cách báo cáo của bạn sẽ hoạt động, vì vậy bạn có thể phải thực hiện một số điều chỉnh. Nhưng tôi nghĩ rằng điều này bao gồm những điều cơ bản.