Có một vài thách thức với câu hỏi này. Các chỉ mục trong SQL Server có thể thực hiện các thao tác sau rất hiệu quả chỉ với một vài lần đọc logic:
- kiểm tra xem một hàng tồn tại
- kiểm tra xem một hàng không tồn tại
- tìm hàng tiếp theo bắt đầu tại một số điểm
- tìm hàng trước bắt đầu tại một số điểm
Tuy nhiên, chúng không thể được sử dụng để tìm hàng thứ N trong một chỉ mục. Làm điều đó đòi hỏi bạn phải cuộn chỉ mục của riêng bạn được lưu dưới dạng bảng hoặc để quét N hàng đầu tiên trong chỉ mục. Mã C # của bạn phụ thuộc rất nhiều vào thực tế là bạn có thể tìm thấy phần tử thứ N của mảng một cách hiệu quả, nhưng bạn không thể làm điều đó ở đây. Tôi nghĩ rằng thuật toán này không thể sử dụng được cho T-SQL mà không thay đổi mô hình dữ liệu.
Thách thức thứ hai liên quan đến các hạn chế về các BINARY
loại dữ liệu. Theo như tôi có thể nói với bạn không thể thực hiện phép cộng, phép trừ hoặc phép chia theo những cách thông thường. Bạn có thể chuyển đổi BINARY(64)
thành a BIGINT
và nó sẽ không ném lỗi chuyển đổi, nhưng hành vi không được xác định :
Chuyển đổi giữa bất kỳ loại dữ liệu và các loại dữ liệu nhị phân không được đảm bảo giống nhau giữa các phiên bản của SQL Server.
Ngoài ra, việc thiếu các lỗi chuyển đổi là một vấn đề ở đây. Bạn có thể chuyển đổi bất cứ thứ gì lớn hơn BIGINT
giá trị lớn nhất có thể nhưng nó sẽ cho bạn kết quả sai.
Đúng là bạn có các giá trị ngay bây giờ lớn hơn 9223372036854775807. Tuy nhiên, nếu bạn luôn bắt đầu từ 1 và tìm kiếm giá trị tối thiểu nhỏ nhất thì các giá trị lớn đó không thể có liên quan trừ khi bảng của bạn có nhiều hơn 9223372036854775807. Điều này dường như không thể xảy ra bởi vì bảng của bạn tại thời điểm đó sẽ vào khoảng 2000 exabyte, vì vậy với mục đích trả lời câu hỏi của bạn, tôi sẽ giả định rằng các giá trị rất lớn không cần phải tìm kiếm. Tôi cũng sẽ thực hiện chuyển đổi loại dữ liệu vì chúng dường như không thể tránh khỏi.
Đối với dữ liệu thử nghiệm, tôi đã chèn tương đương 50 triệu số nguyên liên tiếp vào một bảng cùng với 50 triệu số nguyên khác với một khoảng cách giá trị duy nhất cho mỗi 20 giá trị. Tôi cũng đã chèn một giá trị duy nhất không phù hợp với chữ ký BIGINT
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
Mã đó mất vài phút để chạy trên máy của tôi. Tôi đã làm cho nửa đầu của bảng không có bất kỳ khoảng trống nào để thể hiện một loại trường hợp tồi tệ hơn cho hiệu suất. Mã mà tôi đã sử dụng để giải quyết vấn đề sẽ quét chỉ mục theo thứ tự để nó sẽ hoàn thành rất nhanh nếu khoảng trống đầu tiên xuất hiện sớm trong bảng. Trước khi chúng ta bắt đầu, hãy xác minh rằng dữ liệu phải như sau:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Kết quả cho thấy giá trị tối đa mà chúng tôi chuyển đổi BIGINT
là 102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Có 100 triệu hàng với các giá trị phù hợp với BIGINT như mong đợi:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
Một cách tiếp cận cho vấn đề này là quét chỉ mục theo thứ tự và thoát ngay khi giá trị của một hàng không khớp với ROW_NUMBER()
giá trị mong đợi . Toàn bộ bảng không cần phải quét để có được hàng đầu tiên: chỉ các hàng lên cho đến khoảng trống đầu tiên. Đây là một cách để viết mã có khả năng nhận được kế hoạch truy vấn đó:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
Vì những lý do không phù hợp với câu trả lời này, truy vấn này thường sẽ được chạy bởi seri SQL Server và SQL Server thường sẽ đánh giá thấp số lượng hàng cần quét trước khi tìm thấy kết quả khớp đầu tiên. Trên máy của tôi, SQL Server quét 50000022 hàng từ chỉ mục trước khi tìm thấy kết quả khớp đầu tiên. Truy vấn mất 11 giây để chạy. Lưu ý rằng điều này trả về giá trị đầu tiên vượt qua khoảng cách. Không rõ bạn muốn chính xác hàng nào, nhưng bạn có thể thay đổi truy vấn để phù hợp với nhu cầu của mình mà không gặp nhiều rắc rối. Đây là kế hoạch trông như thế nào:
Ý tưởng khác của tôi là bắt nạt SQL Server sử dụng song song cho truy vấn. Tôi có bốn CPU, vì vậy tôi sẽ chia dữ liệu thành bốn phạm vi và tìm kiếm trên các phạm vi đó. Mỗi CPU sẽ được chỉ định một phạm vi. Để tính toán các phạm vi tôi chỉ cần lấy giá trị tối đa và giả sử rằng dữ liệu được phân phối đều. Nếu bạn muốn thông minh hơn về nó, bạn có thể xem biểu đồ thống kê được lấy mẫu cho các giá trị cột và xây dựng phạm vi của bạn theo cách đó. Mã dưới đây dựa trên rất nhiều thủ thuật không có giấy tờ không an toàn cho sản xuất, bao gồm cả cờ theo dõi 8649 :
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 8649);
Đây là mô hình vòng lặp lồng nhau song song trông như thế nào:
Nhìn chung, truy vấn thực hiện nhiều công việc hơn trước vì nó sẽ quét nhiều hàng hơn trong bảng. Tuy nhiên, bây giờ nó chạy trong 7 giây trên máy tính để bàn của tôi. Nó có thể song song tốt hơn trên một máy chủ thực sự. Đây là một liên kết đến kế hoạch thực tế .
Tôi thực sự không thể nghĩ ra một cách tốt để giải quyết vấn đề này. Thực hiện tính toán bên ngoài SQL hoặc thay đổi mô hình dữ liệu có thể là cược tốt nhất của bạn.
delete
kích hoạt trên bảng sẽ đổ nhị phân hiện có vào một bảng riêng biệt (ví dụcreate table available_for_reuse(id binary64)
:), đặc biệt là theo yêu cầu phải thực hiện tra cứu này rất thường xuyên ?