Xác định vị trí phần tử bị thiếu nhỏ nhất dựa trên một công thức cụ thể


8

Tôi cần có thể xác định vị trí một phần tử bị thiếu từ một bảng có hàng chục triệu hàng và có khóa chính của một BINARY(64)cột (là giá trị đầu vào để tính toán). Các giá trị này hầu hết được chèn theo thứ tự, nhưng đôi khi tôi muốn sử dụng lại một giá trị trước đó đã bị xóa. Không thể sửa đổi các bản ghi bị xóa bằng một IsDeletedcột, vì đôi khi một hàng được chèn vào có hàng triệu giá trị trước các hàng hiện có. Điều này có nghĩa là dữ liệu mẫu sẽ trông giống như:

KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF

Vì vậy, việc chèn tất cả các giá trị còn thiếu giữa 0x0000000000020xFFFFFFFFFFFFlà không thể thực hiện được, lượng thời gian và không gian được sử dụng sẽ là không mong muốn. Về cơ bản, khi tôi chạy thuật toán, tôi hy vọng nó sẽ quay trở lại 0x000000000003, đây là lần mở đầu tiên.

Tôi đã đưa ra một thuật toán tìm kiếm nhị phân trong C #, sẽ truy vấn cơ sở dữ liệu cho từng giá trị tại vị trí ivà kiểm tra xem giá trị đó có được mong đợi hay không. Đối với ngữ cảnh, thuật toán khủng khiếp của tôi: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula

Thuật toán này sẽ chạy, ví dụ, 26-27 truy vấn SQL trên một bảng có 100.000.000 mục. (Điều đó dường như không nhiều, nhưng nó sẽ xảy ra rất thường xuyên.) Hiện tại, bảng này có khoảng 50.000.000 hàng trong đó và hiệu suất đang trở nên đáng chú ý .

Suy nghĩ thay thế đầu tiên của tôi là dịch cái này sang một thủ tục được lưu trữ, nhưng nó có những rào cản riêng. (Tôi phải viết một BINARY(64) + BINARY(64)thuật toán, cũng như một loạt các thứ khác.) Điều này sẽ gây đau đớn, nhưng không phải là không thể. Tôi cũng đã xem xét việc thực hiện thuật toán dịch dựa trên ROW_NUMBER, nhưng tôi có cảm giác rất tệ về việc này. (A BIGINTkhông đủ lớn cho các giá trị này.)

Tôi sẵn sàng cho các đề xuất khác , vì tôi thực sự cần điều này càng nhanh càng tốt. Đối với những gì đáng giá cột duy nhất được chọn bởi truy vấn C # là KeyCol, các cột khác không liên quan đến phần này.


Ngoài ra, với giá trị của nó, truy vấn hiện tại tìm nạp bản ghi thích hợp nằm dọc theo dòng:

SELECT [KeyCol]
  FROM [Table]
  ORDER BY [KeyCol] ASC
  OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY

Đâu <VALUE>là chỉ số được cung cấp bởi thuật toán. Tôi cũng chưa có BIGINTvấn đề OFFSETgì, nhưng tôi sẽ làm. (Chỉ có 50.000.000 hàng ngay bây giờ có nghĩa là nó không bao giờ yêu cầu chỉ số cao hơn giá trị đó, nhưng đến một lúc nào đó, nó sẽ vượt quá BIGINTphạm vi.)

Một số dữ liệu bổ sung:

  • Từ xóa, gap:sequentialtỷ lệ là về 1:20;
  • 35.000 hàng cuối cùng trong bảng có giá trị BIGINTtối đa > ;

Tìm kiếm một chút rõ ràng hơn ... 1) tại sao bạn cần nhị phân có sẵn 'nhỏ nhất' thay vì bất kỳ nhị phân có sẵn nào? 2) về phía trước, bất kỳ cơ hội nào để đặt một deletekí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 ?
markp-fuso

@markp Giá trị khả dụng nhỏ nhất có "ưu tiên" đối với nó, hãy nghĩ rằng nó tương tự như trình rút ngắn URL, bạn không muốn giá trị dài hơn tiếp theo , bởi vì ai đó có thể chỉ định thủ công một cái gì mynameisebrownđó có nghĩa là bạn sẽ nhận được mynameisebrowo, mà bạn sẽ nhận được sẽ không muốn nếu abccó sẵn.
Der Kommissar

Một truy vấn như select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows onlycung cấp cho bạn những gì?
Lennart

@Lennart Không phải thứ tôi cần. Phải sử dụng SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol], mà luôn luôn trở lại 1.
Der Kommissar

Tôi tự hỏi liệu đó có phải là một loại lỗi truyền không, nó không nên trả về 1. Chọn t1.keycol từ ... return là gì?
Lennart

Câu trả lời:


6

Tóm lại, Joe đã đạt được hầu hết các điểm mà tôi vừa dành một giờ để đánh máy, tóm lại:

  • rất nghi ngờ bạn sẽ hết KeyColgiá trị < bigintmax (9.2e18), do đó, chuyển đổi (nếu cần) thành / từ bigintkhông nên là vấn đề miễn là bạn giới hạn tìm kiếmKeyCol <= 0x00..007FFFFFFFFFFFFFFF
  • Tôi không thể nghĩ ra một truy vấn sẽ "hiệu quả" mọi lúc mọi nơi; bạn có thể gặp may mắn và tìm thấy một khoảng trống gần khi bắt đầu tìm kiếm của bạn, hoặc bạn có thể phải trả giá đắt để tìm ra khoảng trống khá nhiều cách trong tìm kiếm của bạn
  • Trong khi tôi suy nghĩ ngắn gọn về cách song song hóa truy vấn, tôi đã nhanh chóng loại bỏ ý tưởng đó (với tư cách là một DBA, tôi không muốn phát hiện ra rằng quy trình của bạn thường xuyên làm chậm trình lưu trữ dữ liệu của tôi với việc sử dụng cpu 100% ... đặc biệt là nếu bạn có thể có nhiều bản sao này chạy cùng một lúc); không ... song song sẽ ra khỏi câu hỏi

Vậy lam gi?

Chúng ta hãy giữ ý tưởng tìm kiếm (lặp đi lặp lại, thâm dụng cpu, mạnh mẽ) trong một phút và nhìn vào bức tranh lớn hơn.

  • trên cơ sở trung bình, một ví dụ của tìm kiếm này sẽ cần quét hàng triệu khóa chỉ mục (và yêu cầu một chút cpu, đập bộ đệm db và người dùng đang xem kính giờ quay) chỉ để xác định một giá trị duy nhất
  • nhân số cpu-cách sử dụng / bộ nhớ cache đập / quay-giờ-kính với ... bạn mong đợi bao nhiêu tìm kiếm trong một ngày?
  • Hãy nhớ rằng, nói chung, mỗi phiên bản của tìm kiếm này sẽ cần quét cùng một bộ (hàng triệu) khóa chỉ mục; đó là RẤT NHIỀU hoạt động lặp đi lặp lại vì lợi ích tối thiểu như vậy

Điều tôi muốn đề xuất là một số bổ sung cho mô hình dữ liệu ...

  • một bảng mới theo dõi một tập hợp các KeyColgiá trị 'có sẵn để sử dụng' , ví dụ:available_for_use(KeyCol binary(64) not null primary key)
  • có bao nhiêu hồ sơ bạn duy trì trong bảng này tùy thuộc vào bạn để quyết định, ví dụ, có lẽ đủ cho hoạt động của một tháng?
  • bảng có thể định kỳ (hàng tuần?) được 'đứng đầu' với một loạt các KeyColgiá trị mới (có thể tạo ra một 'lưu trữ trên cùng' được lưu trữ?) [ví dụ: cập nhật select/top/row_number()truy vấn của Joe để thực hiện top 100000]
  • bạn có thể thiết lập quy trình giám sát để theo dõi số lượng mục có sẵn available_for_use chỉ trong trường hợp bạn bắt đầu cạn kiệt giá trị
  • một trình kích hoạt XÓA mới (hoặc đã sửa đổi) trên> main_table <đặt KeyColcác giá trị đã xóa vào bảng mới của chúng tôi available_for_usebất cứ khi nào một hàng bị xóa khỏi bảng chính
  • nếu bạn cho phép cập nhật KeyColcột thì trình kích hoạt CẬP NHẬT mới / được sửa đổi trên> main_table <để giữ cho bảng mới của chúng tôi available_for_useđược cập nhật
  • khi đến lúc 'tìm kiếm' cho một KeyColgiá trị mới, bạn select min(KeyCol) from available_for_userõ ràng sẽ có thêm một chút về điều này vì a) bạn sẽ cần mã cho các vấn đề tương tranh - không muốn 2 bản sao của quy trình của bạn giống nhau min(KeyCol)và b) bạn Sẽ cần phải xóa min(KeyCol)khỏi bảng; mã này tương đối dễ, có thể là mã lưu trữ và có thể được xử lý trong một câu hỏi và trả lời khác nếu cần thiết)
  • trong trường hợp xấu nhất, nếu select min(KeyCol)quy trình của bạn không tìm thấy hàng nào có sẵn, bạn có thể khởi động công cụ 'top off' của mình để tạo ra một loạt hàng mới

Với những thay đổi được đề xuất cho mô hình dữ liệu:

  • bạn loại bỏ RẤT NHIỀU chu kỳ cpu quá mức [DBA của bạn sẽ cảm ơn bạn]
  • bạn loại bỏ TẤT CẢ những lần quét chỉ mục lặp đi lặp lại và đập bộ đệm [DBA của bạn sẽ cảm ơn bạn]
  • người dùng của bạn không còn phải xem kính giờ quay (mặc dù họ có thể không thích mất lý do để rời khỏi bàn làm việc của họ)
  • có rất nhiều cách để theo dõi kích thước của available_for_usebảng để đảm bảo bạn không bao giờ hết giá trị mới

Có, available_for_usebảng đề xuất chỉ là một bảng các giá trị 'khóa tiếp theo' được tạo trước; và vâng, có khả năng xảy ra tranh chấp khi lấy giá trị 'tiếp theo', nhưng bất kỳ sự tranh chấp nào a) đều được giải quyết dễ dàng thông qua thiết kế bảng / chỉ mục / truy vấn thích hợp và b) sẽ không đáng kể so với chi phí / sự chậm trễ với ý tưởng hiện tại lặp đi lặp lại, lực lượng vũ phu, tìm kiếm chỉ mục.


Điều này thực sự giống với suy nghĩ của tôi khi trò chuyện, tôi nghĩ có lẽ cứ sau 15-20 phút, vì truy vấn của Joe chạy khá nhanh (trên máy chủ trực tiếp với trường hợp xấu nhất về dữ liệu thử nghiệm là 4,5 giây, tốt nhất là 0,25 giây), tôi có thể lấy các khóa có giá trị trong một ngày và không ít hơn ncác khóa (có thể là 10 hoặc 20, để buộc nó tìm kiếm những gì có thể thấp hơn, giá trị mong muốn hơn). Thực sự đánh giá cao câu trả lời ở đây mặc dù, bạn đặt những suy nghĩ bằng văn bản! :)
Der Kommissar

ahhhh, nếu bạn đã có một máy chủ ứng dụng / máy chủ trung gian có thể cung cấp bộ đệm trung gian của các KeyColgiá trị khả dụng ... vâng, điều đó cũng sẽ hoạt động :-) và rõ ràng loại bỏ sự cần thiết phải thay đổi mô hình dữ liệu eh
markp-fuso

Chính xác, tôi đang suy nghĩ về việc xây dựng bộ đệm tĩnh trên chính ứng dụng web, vấn đề duy nhất là nó bị phân tán (vì vậy tôi cần đồng bộ hóa bộ đệm giữa các máy chủ), điều đó có nghĩa là việc triển khai SQL hoặc kho trung gian sẽ nhiều ưa thích. :)
Der Kommissar

hmmmm ... một trình KeyColquản lý phân tán và cần mã hóa các vi phạm PK tiềm ẩn nếu 2 (hoặc nhiều hơn) các ứng dụng đồng thời thử sử dụng cùng một KeyColgiá trị ... yuck ... chắc chắn dễ dàng hơn với một máy chủ phần mềm trung gian hoặc một giải pháp db-centric
markp-fuso

8

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 BINARYloạ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 BIGINTvà 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 BIGINTgiá 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 BIGINTlà 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:

kế hoạch nối tiếp

Ý 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:

kế hoạch song song

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.


Ngay cả khi câu trả lời tốt nhất là "điều này sẽ không hoạt động tốt trong SQL", thì ít nhất nó cũng cho tôi biết nơi cần di chuyển tiếp theo. :)
Der Kommissar

1

Đây là một câu trả lời có thể sẽ không hiệu quả với bạn, nhưng dù sao tôi cũng sẽ thêm nó vào.

Mặc dù BINary (64) là vô số nhưng có sự hỗ trợ kém để xác định sự kế thừa của một mặt hàng. Vì BIGINT dường như quá nhỏ so với tên miền của bạn, bạn có thể cân nhắc sử dụng DECIMAL (38,0), dường như là loại SỐ lớn nhất trong máy chủ SQL.

CREATE TABLE SearchTestTableProper
( keycol decimal(38,0) not null primary key );

INSERT INTO SearchTestTableProper (keycol)
VALUES (1),(2),(3),(12);

Tìm khoảng cách đầu tiên thật dễ dàng vì chúng ta có thể xây dựng số chúng ta đang tìm kiếm:

select top 1 t1.keycol+1 
from SearchTestTableProper t1 
where not exists (
    select 1 
    from SearchTestTableProper t2 
    where t2.keycol = t1.keycol + 1
)
order by t1.keycol;

Một phép nối vòng lặp lồng nhau trên chỉ số pk phải đủ để tìm mục có sẵn đầu tiên.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.