Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Xử lý ngày và giờ thay vì datetime

Các chuyên gia dữ liệu không phải lúc nào cũng sử dụng cơ sở dữ liệu có thiết kế tối ưu. Đôi khi những điều khiến bạn khóc lại là những điều chúng ta đã tự làm vì chúng dường như là những ý tưởng hay vào thời điểm đó. Đôi khi chúng là do các ứng dụng của bên thứ ba. Đôi khi họ chỉ đơn giản là trước bạn.

Điều tôi đang nghĩ đến trong bài đăng này là khi cột datetime (hoặc datetime2, hay tốt hơn là datetimeoffset) của bạn thực sự là hai cột - một cho ngày và một cho giờ. (Nếu bạn lại có một cột riêng để bù đắp, thì tôi sẽ ôm bạn vào lần sau khi gặp bạn, vì có lẽ bạn đã phải đối mặt với đủ loại tổn thương.)

Tôi đã thực hiện một cuộc khảo sát trên Twitter và thấy rằng đây là một vấn đề rất thực tế mà thỉnh thoảng khoảng một nửa số bạn phải giải quyết vấn đề ngày giờ.

AdventureWorks gần như làm được điều này - nếu bạn nhìn vào bảng Sales.SalesOrderHeader, bạn sẽ thấy một cột ngày giờ gọi là OrderDate, cột này luôn có các ngày tháng chính xác trong đó. Tôi cá rằng nếu bạn là nhà phát triển báo cáo tại AdventureWorks, bạn có thể đã viết các truy vấn tìm kiếm số lượng đơn đặt hàng vào một ngày cụ thể, sử dụng GROUP BY OrderDate hoặc tương tự như vậy. Ngay cả khi bạn biết rằng đây là cột ngày giờ và có khả năng nó cũng lưu trữ thời gian không phải nửa đêm, bạn vẫn sẽ nói GROUP BY OrderDate chỉ vì mục đích sử dụng chỉ mục đúng cách. GROUP BY CAST (OrderDate AS DATE) không cắt giảm nó.

Tôi có một chỉ mục trên OrderDate, giống như bạn làm nếu bạn thường xuyên truy vấn cột đó và tôi có thể thấy rằng việc nhóm theo CAST (OrderDate AS DATE) kém hơn khoảng bốn lần từ góc độ CPU.

Vì vậy, tôi hiểu lý do tại sao bạn sẽ rất vui khi truy vấn cột của mình như thể đó là một ngày, chỉ đơn giản là biết rằng bạn sẽ gặp khó khăn nếu việc sử dụng cột đó thay đổi. Có thể bạn giải quyết điều này bằng cách có một ràng buộc trên bàn. Có thể bạn chỉ chúi đầu vào cát.

Và khi ai đó đi cùng và nói "Bạn biết đấy, chúng tôi cũng nên lưu trữ thời gian mà các đơn đặt hàng xảy ra", thì bạn sẽ nghĩ đến tất cả các mã giả định Ngày đặt hàng chỉ đơn giản là một ngày và hình dung rằng có một cột riêng được gọi là Thời gian đặt hàng (kiểu dữ liệu của thời gian, xin vui lòng) sẽ là lựa chọn hợp lý nhất. Tôi hiểu. Nó không phải là lý tưởng, nhưng nó hoạt động mà không làm hỏng quá nhiều thứ.

Tại thời điểm này, tôi khuyên bạn cũng nên tạo OrderDateTime, đây sẽ là một cột được tính toán kết hợp cả hai (bạn nên thực hiện bằng cách thêm số ngày kể từ ngày 0 vào CAST (OrderDate dưới dạng datetime2), thay vì cố gắng thêm thời gian vào ngày, thường là một mớ hỗn độn hơn rất nhiều). Và sau đó lập chỉ mục OrderDateTime, bởi vì điều đó sẽ hợp lý.

Nhưng khá thường xuyên, bạn sẽ thấy ngày và giờ là các cột riêng biệt, về cơ bản bạn không thể làm gì với nó. Bạn không thể thêm cột được tính toán vì đó là ứng dụng của bên thứ ba và bạn không biết điều gì có thể bị hỏng. Bạn có chắc họ không bao giờ làm CHỌN * không? Một ngày nào đó, tôi hy vọng họ sẽ cho phép chúng tôi thêm cột và ẩn chúng đi, nhưng hiện tại, bạn chắc chắn có nguy cơ phá vỡ nội dung.

Và, bạn biết đấy, ngay cả msdb cũng làm được điều này. Cả hai đều là số nguyên. Và đó là do khả năng tương thích ngược, tôi cho là vậy. Nhưng tôi nghi ngờ bạn đang xem xét việc thêm một cột được tính toán vào một bảng trong msdb.

Vì vậy, làm thế nào để chúng tôi truy vấn điều này? Giả sử chúng ta muốn tìm các mục nhập trong một phạm vi ngày giờ cụ thể?

Hãy làm một số thử nghiệm.

Đầu tiên, hãy tạo một bảng có 3 triệu hàng và lập chỉ mục các cột mà chúng ta quan tâm.

select identity(int,1,1) as ID, OrderDate, 
  dateadd(minute, abs(checksum(newid())) % (60 * 24), cast('00:00' as time)) as OrderTime
into dbo.Sales3M
from Sales.SalesOrderHeader
cross apply (select top 100 * from master..spt_values) v;
 
create index ixDateTime on dbo.Sales3M (OrderDate, OrderTime) include (ID);

(Tôi có thể đã tạo chỉ mục đó theo nhóm, nhưng tôi nhận thấy rằng chỉ mục không được phân nhóm là điển hình hơn cho môi trường của bạn.)

Dữ liệu của chúng tôi trông như thế này và tôi muốn tìm các hàng giữa, ví dụ, ngày 2 tháng 8 năm 2011 lúc 8:30 và ngày 5 tháng 8 năm 2011 lúc 21:30.

Bằng cách xem qua dữ liệu, tôi có thể thấy rằng tôi muốn tất cả các hàng trong khoảng từ 48221 đến 50171. Đó là 50171-48221 + 1 =1951 hàng (+1 là vì nó là một phạm vi bao gồm). Điều này giúp tôi tự tin rằng kết quả của tôi là chính xác. Bạn có thể có tương tự trên máy của mình, nhưng không chính xác, vì tôi đã sử dụng các giá trị ngẫu nhiên khi tạo bảng của mình.

Tôi biết rằng tôi không thể chỉ làm điều gì đó như thế này:

select * 
from dbo.Sales3M 
where OrderDate between '20110802' and '20110805'
and OrderTime between '8:30' and '21:30';

… Bởi vì điều này sẽ không bao gồm điều gì đó đã xảy ra qua đêm vào ngày 4. Điều này mang lại cho tôi 1268 hàng - rõ ràng là không đúng.

Một tùy chọn là kết hợp các cột:

select * 
from dbo.Sales3M 
where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) 
  between '20110802 8:30' and '20110805 21:30';

Điều này cho kết quả chính xác. Nó có. Chỉ là điều này hoàn toàn không thể phân loại và cung cấp cho chúng tôi một bản Quét trên tất cả các hàng trong bảng của chúng tôi. Trên 3 triệu hàng của chúng tôi, có thể mất vài giây để chạy điều này.

Vấn đề của chúng ta là chúng ta có một trường hợp bình thường, và hai trường hợp đặc biệt. Chúng tôi biết rằng mỗi hàng thỏa mãn OrderDate> ‘20110802’ VÀ OrderDate <‘20110805’ là một hàng mà chúng tôi muốn. Nhưng chúng tôi cũng cần mọi hàng vào hoặc sau 8:30 ngày 20110802 và trước 21:30 ngày 20110805. Và điều đó dẫn chúng tôi đến:

select * 
from dbo.Sales3M 
where (OrderDate > '20110802' and OrderDate < '20110805')
or (OrderDate = '20110802' and OrderTime >= '8:30')
or (OrderDate = '20110805' and OrderTime <= '21:30');

HOẶC là khủng khiếp, tôi biết. Nó cũng có thể dẫn đến Quét, mặc dù không nhất thiết. Ở đây tôi thấy ba Tìm kiếm chỉ mục, được nối với nhau và sau đó được kiểm tra tính duy nhất. Trình tối ưu hóa truy vấn rõ ràng nhận ra rằng nó không nên trả lại cùng một hàng hai lần, nhưng không nhận ra rằng ba điều kiện loại trừ lẫn nhau. Và trên thực tế, nếu bạn làm điều này trong một phạm vi trong vòng một ngày, bạn sẽ nhận được kết quả sai.

Chúng tôi có thể sử dụng UNION ALL vào việc này, điều đó có nghĩa là QO sẽ không quan tâm liệu các điều kiện có loại trừ lẫn nhau hay không. Điều này cung cấp cho chúng tôi ba Tìm kiếm được nối với nhau - điều đó khá tốt.

select * 
from dbo.Sales3M 
where (OrderDate > '20110802' and OrderDate < '20110805')
union all 
select * 
from dbo.Sales3M 
where (OrderDate = '20110802' and OrderTime >= '8:30')
union all 
select * 
from dbo.Sales3M 
where (OrderDate = '20110805' and OrderTime <= '21:30');

Nhưng đó vẫn là ba tìm kiếm. Thống kê IO cho tôi biết có 20 lần đọc trên máy của tôi.

Bây giờ, khi tôi nghĩ về khả năng phân chia, tôi không chỉ nghĩ đến việc tránh đặt các cột chỉ mục bên trong các biểu thức, tôi còn nghĩ về điều gì có thể giúp ích cho điều gì đó dường như đáng sợ.

Lấy ví dụ như WHERE LastName LIKE 'Far%'. Khi tôi xem xét kế hoạch cho việc này, tôi thấy một Tìm kiếm, với một Vị từ Tìm kiếm đang tìm kiếm bất kỳ tên nào từ Xa đến (nhưng không bao gồm) FaS. Và sau đó có một Vị từ dư kiểm tra điều kiện LIKE. Điều này không phải vì QO cho rằng LIKE là có thể phân biệt được. Nếu có, nó sẽ có thể sử dụng LIKE trong Tìm kiếm vị từ. Đó là bởi vì nó biết rằng mọi thứ được thỏa mãn bởi điều kiện LIKE đó phải nằm trong phạm vi đó.

Lấy WHERE CAST (OrderDate AS DATE) ='20110805'

Ở đây, chúng ta thấy một Dự đoán tìm kiếm tìm kiếm các giá trị OrderDate giữa hai giá trị đã được thực hiện ở những nơi khác trong kế hoạch, nhưng tạo ra một phạm vi trong đó các giá trị phù hợp phải tồn tại. Đây không phải là> =20110805 00:00 và <20110806 00:00 (đó là những gì tôi đã tạo ra nó), nó là một cái gì đó khác. Giá trị bắt đầu của dải ô này phải nhỏ hơn 20110805 00:00, vì nó là>, không phải> =. Tất cả những gì chúng ta thực sự có thể nói là khi ai đó trong Microsoft triển khai cách QO nên phản hồi với loại vị từ này, họ đã cung cấp cho nó đủ thông tin để đưa ra cái mà tôi gọi là “vị từ trợ giúp”.

Bây giờ, tôi rất muốn Microsoft tạo ra nhiều chức năng có thể phân loại hơn, nhưng yêu cầu cụ thể đó đã bị Đóng rất lâu trước khi họ gỡ bỏ Connect.

Nhưng có lẽ ý tôi là họ tạo ra nhiều vị từ trợ giúp hơn.

Vấn đề với các vị từ trợ giúp là chúng gần như chắc chắn đọc nhiều hàng hơn bạn muốn. Nhưng vẫn tốt hơn là xem qua toàn bộ chỉ mục.

Tôi biết rằng tất cả các hàng tôi muốn trả lại sẽ có Ngày đặt hàng trong khoảng từ 20110802 đến 20110805. Chỉ là có một số hàng tôi không muốn.

Tôi chỉ có thể xóa chúng và điều này sẽ hợp lệ:

select *
from dbo.Sales3M
where OrderDate between '20110802' and '20110805'
and not (OrderDate = '20110802' and OrderTime < '8:30')
and not (OrderDate = '20110805' and OrderTime > '21:30');

Nhưng tôi cảm thấy đây là một giải pháp đòi hỏi một số nỗ lực suy nghĩ để đưa ra. Phía nhà phát triển cần ít nỗ lực hơn là chỉ cần cung cấp một vị từ trợ giúp cho phiên bản chính xác nhưng chậm của chúng tôi.

select * 
from dbo.Sales3M 
where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) 
between '20110802 8:30' and '20110805 21:30'
and OrderDate between '20110802' and '20110805';

Cả hai truy vấn này đều tìm 2300 hàng vào đúng ngày và sau đó cần kiểm tra tất cả các hàng đó so với các vị từ khác. Một người phải kiểm tra hai điều kiện NOT, điều kiện kia phải thực hiện một số chuyển đổi kiểu và toán học. Nhưng cả hai đều nhanh hơn nhiều so với những gì chúng tôi đã có trước đây, và thực hiện một Tìm kiếm duy nhất (13 lần đọc). Chắc chắn, tôi nhận được cảnh báo về một RangeScan không hiệu quả, nhưng đây là sở thích của tôi hơn là thực hiện ba cái hiệu quả.

Theo một số cách, vấn đề lớn nhất với ví dụ cuối cùng này là một số người có ý tốt sẽ thấy rằng vị từ trợ giúp là thừa và có thể xóa nó. Đây là trường hợp với tất cả các vị từ trợ giúp. Vì vậy, hãy đưa một bình luận vào.

select * 
from dbo.Sales3M 
where dateadd(day,datediff(day,0,OrderDate),cast(OrderTime as datetime2)) 
  between '20110802 8:30' and '20110805 21:30'
/* This next predicate is just a helper to improve performance */
and OrderDate between '20110802' and '20110805';

Nếu bạn có thứ gì đó không phù hợp với vị từ đẹp mắt, hãy tìm ra thứ đó và sau đó tìm ra thứ bạn cần loại trừ khỏi nó. Bạn có thể nghĩ ra một giải pháp tốt hơn.

@rob_farley


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Địa chỉ OGG-01224 đã được sử dụng

  2. Tạo mô hình nâng cao hơn với trạng thái người dùng, chủ đề và bài đăng

  3. WhoIsActive Runner

  4. Hướng dẫn giao dịch SQL

  5. Cách tính tuổi từ ngày sinh trong SQL