Nguyên tắc "Đừng lặp lại chính mình" gợi ý rằng bạn nên giảm sự lặp lại. Tuần này, tôi đã gặp một trường hợp mà DRY nên được ném ra ngoài cửa sổ. Cũng có những trường hợp khác (ví dụ, các hàm vô hướng), nhưng trường hợp này là một trường hợp thú vị liên quan đến logic Bitwise.
Hãy hình dung bảng sau:
CREATE TABLE dbo.CarOrders ( OrderID INT PRIMARY KEY, WheelFlag TINYINT, OrderDate DATE --, ... other columns ... ); CREATE INDEX IX_WheelFlag ON dbo.CarOrders(WheelFlag);
Các bit "WheelFlag" đại diện cho các tùy chọn sau:
0 = stock wheels 1 = 17" wheels 2 = 18" wheels 4 = upgraded tires
Vì vậy, các kết hợp có thể có là:
0 = no upgrade 1 = upgrade to 17" wheels only 2 = upgrade to 18" wheels only 4 = upgrade tires only 5 = 1 + 4 = upgrade to 17" wheels and better tires 6 = 2 + 4 = upgrade to 18" wheels and better tires
Ít nhất là bây giờ hãy gạt các tranh luận sang một bên về việc liệu điều này có nên được đóng gói thành một TINYINT duy nhất ngay từ đầu hay được lưu trữ dưới dạng các cột riêng biệt hoặc sử dụng mô hình EAV… sửa thiết kế là một vấn đề riêng biệt. Đây là về cách làm việc với những gì bạn có.
Để làm cho các ví dụ hữu ích, hãy điền vào bảng này với một loạt dữ liệu ngẫu nhiên. (Và chúng tôi sẽ giả định rằng, để đơn giản, bảng này chỉ chứa các đơn đặt hàng chưa được giao.) Điều này sẽ chèn 50.000 hàng có phân phối gần như bằng nhau giữa sáu kết hợp tùy chọn:
;WITH n AS ( SELECT n,Flag FROM (VALUES(1,0),(2,1),(3,2),(4,4),(5,5),(6,6)) AS n(n,Flag) ) INSERT dbo.CarOrders ( OrderID, WheelFlag, OrderDate ) SELECT x.rn, n.Flag, DATEADD(DAY, x.rn/100, '20100101') FROM n INNER JOIN ( SELECT TOP (50000) n = (ABS(s1.[object_id]) % 6) + 1, rn = ROW_NUMBER() OVER (ORDER BY s2.[object_id]) FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 ) AS x ON n.n = x.n;
Nếu chúng ta nhìn vào bảng phân tích, chúng ta có thể thấy sự phân bổ này. Lưu ý rằng kết quả của bạn có thể hơi khác so với kết quả của tôi tùy thuộc vào các đối tượng trong hệ thống của bạn:
SELECT WheelFlag, [Count] = COUNT(*) FROM dbo.CarOrders GROUP BY WheelFlag;
Kết quả:
WheelFlag Count --------- ----- 0 7654 1 8061 2 8757 4 8682 5 8305 6 8541
Bây giờ, giả sử là thứ Ba và chúng tôi vừa có một lô hàng bánh xe 18 ", trước đó đã hết hàng. Điều này có nghĩa là chúng tôi có thể đáp ứng tất cả các đơn đặt hàng yêu cầu bánh xe 18" - cả những loại bánh xe nâng cấp (6), và những cái không (2). Vì vậy, chúng tôi * có thể * viết một truy vấn như sau:
SELECT OrderID FROM dbo.CarOrders WHERE WheelFlag IN (2,6);
Trong cuộc sống thực, tất nhiên, bạn không thể thực sự làm được điều đó; Điều gì sẽ xảy ra nếu nhiều tùy chọn được thêm vào sau đó, như khóa bánh xe, bảo hành bánh xe trọn đời hoặc nhiều tùy chọn lốp xe? Bạn không muốn phải viết một chuỗi giá trị IN () cho mọi kết hợp có thể. Thay vào đó, chúng ta có thể viết một phép toán BITWISE AND, để tìm tất cả các hàng nơi bit thứ 2 được đặt, chẳng hạn như:
DECLARE @Flag TINYINT = 2; SELECT OrderID FROM dbo.CarOrders WHERE WheelFlag & @Flag = @Flag;
Điều này cho tôi kết quả tương tự như truy vấn IN (), nhưng nếu tôi so sánh chúng bằng cách sử dụng SQL Sentry Plan Explorer, thì hiệu suất khá khác biệt:
Thật dễ dàng để hiểu tại sao. Đầu tiên sử dụng một tìm kiếm chỉ mục để tách các hàng đáp ứng truy vấn, với một bộ lọc trên cột WheelFlag:
Phương thức thứ hai sử dụng quá trình quét, cùng với một chuyển đổi ngầm và các số liệu thống kê không chính xác. Tất cả là do toán tử BITWISE AND:
Vì vậy, điều này có nghĩa là gì? Trọng tâm của nó, điều này cho chúng ta biết rằng hoạt động BITWISE AND không thể phân biệt được .
Nhưng tất cả hy vọng không bị mất.
Nếu chúng ta bỏ qua nguyên tắc DRY trong giây lát, chúng ta có thể viết một truy vấn hiệu quả hơn một chút bằng cách hơi thừa để tận dụng chỉ mục trên cột WheelFlag. Giả sử rằng chúng ta đang theo đuổi bất kỳ tùy chọn WheelFlag nào trên 0 (không nâng cấp gì cả), chúng ta có thể viết lại truy vấn theo cách này, nói với SQL Server rằng giá trị WheelFlag ít nhất phải có cùng giá trị với cờ (loại bỏ 0 và 1 ), và sau đó thêm thông tin bổ sung mà nó cũng phải chứa cờ đó (do đó loại bỏ 5).
SELECT OrderID FROM dbo.CarOrders WHERE WheelFlag >= @Flag AND WheelFlag & @Flag = @Flag;
Phần> =của điều khoản này rõ ràng được bao phủ bởi phần BITWISE, vì vậy đây là nơi chúng tôi vi phạm DRY. Nhưng vì mệnh đề này chúng tôi đã thêm là có thể phân loại, nên việc chuyển hoạt động BITWISE AND thành điều kiện tìm kiếm phụ vẫn mang lại kết quả tương tự và truy vấn tổng thể mang lại hiệu suất tốt hơn. Chúng tôi thấy một chỉ mục tương tự đang tìm kiếm phiên bản được mã hóa cứng của truy vấn ở trên và trong khi các ước tính thậm chí còn thấp hơn nữa (điều gì đó có thể được giải quyết như một vấn đề riêng biệt), số lần đọc vẫn thấp hơn so với chỉ hoạt động BITWISE AND:
Chúng ta cũng có thể thấy rằng một bộ lọc được sử dụng chống lại chỉ mục, mà chúng ta không thấy khi sử dụng riêng phép toán BITWISE AND:
Kết luận
Đừng ngại lặp lại chính mình. Đôi khi thông tin này có thể giúp ích cho trình tối ưu hóa; mặc dù có thể không hoàn toàn trực quan với tiêu chí * thêm * để cải thiện hiệu suất, nhưng điều quan trọng là phải hiểu khi nào các mệnh đề bổ sung giúp giảm bớt dữ liệu cho kết quả cuối cùng hơn là giúp trình tối ưu hóa dễ dàng tìm thấy các hàng chính xác của riêng nó.