Có một triển khai SQL Server của vấn đề Chuỗi con chung dài nhất không?


7

Có một triển khai SQL Server của vấn đề Chuỗi con chung dài nhất không? Một giải pháp kiểm tra tất cả các hàng của một cột trong SQL Server? Tôi đã thấy các giải pháp lấy hai chuỗi làm đầu vào, nhưng không có giải pháp SQL Server nào xem xét tất cả các hàng của một cột trong bảng.

Tôi đã thử một vài điều, nhưng thành thật mà nói tôi nghĩ rằng một giải pháp đi qua đầu tôi vào lúc này, vì vậy mọi đề xuất đều được chào đón.

Không có vấn đề "thế giới thực" ở đây, tôi chỉ xem xét các vấn đề lập trình và cách giải quyết chúng với SQL Server.


3
Đây có vẻ không phải là một vấn đề tốt đối với cơ sở dữ liệu để giải quyết, thành thật mà nói.
Aaron Bertrand

ManOnAMisson: chỉ là FYI, tôi đã thêm phần Cập nhật cuối cùng bao gồm một liên kết đến tập lệnh thử nghiệm, hiện bao gồm phiên bản T-SQL được cập nhật (được điều chỉnh từ mã của MisterMagoo) đôi khi nhanh hơn UDA của SQLCLR. Thêm chi tiết trong câu trả lời của tôi :).
Solomon Rutzky 17/03/2016

Câu trả lời:


7

Điều này có thể thực hiện được khá dễ dàngdưới dạng Tập hợp do Người dùng Xác định SQLCLR (UDA). Một tập hợp hoạt động trên một tập hợp các hàng, vì vậy bạn sẽ có thể hoạt động trên tất cả các hàng hoặc chỉ là một tập hợp con, dựa trên một WHEREđiều kiện và tùy chọn GROUP BY(nếu muốn hoạt động trên các bộ hàng riêng biệt).

NHƯNG, bạn có nên làm điều này hay không phụ thuộc vào những gì bạn đang dự định làm với kết quả. Nếu đây là một dự án một lần để thực hiện một số nghiên cứu sẽ không được lặp lại, thì có lẽ tốt nhất là chỉ cần tạo một Ứng dụng Console nhỏ để đọc theo hàng và xử lý theo đó.

Tuy nhiên, nếu bạn có một số nhu cầu sử dụng giá trị được trả về trong một quy trình tập trung vào cơ sở dữ liệu, thì SQLCLR sẽ ổn (giả sử bạn làm theo hai khuyến nghị được đề cập trong phần "Có thể sử dụng các thủ thuật sau để giảm mức sử dụng bộ nhớ của việc triển khai" trong phần Mã giả . Bạn sẽ chỉ cần tìm cách xử lý "sáng tạo" với tình huống có nhiều kết quả cho chuỗi con chung dài nhất (nghĩa là nếu 2 hoặc nhiều chuỗi con chung buộc cho "vị trí đầu tiên"). ví dụ từ trang Wikipedia (hiển thị 2 kết quả khớp cho 2 chuỗi):

ABAB
BABA

Trả về cả hai:

  • BAB
  • ABA

Có lẽ trả về một tài liệu XML trùng khớp vì đó là phân tích cú pháp và có thể chứa bất kỳ chuỗi nào (khi được thoát đúng cách).


CẬP NHẬT (cập nhật và cập nhật lại)

Mã nguồn .NET C # cho điều này có thể được tìm thấy trên Pastebin.com tại:

SQLCLR UDA cho chuỗi con chung dài nhất - Mã nguồn

Và đối với bất kỳ ai muốn chơi với UDA này mà không biên dịch nó, có thể tìm thấy tập lệnh T-SQL cài đặt (không có DLL bên ngoài) trên Pastebin.com tại:

SQLCLR UDA cho chuỗi con chung dài nhất - Trình cài đặt

UDA nên khá hiệu quả về bộ nhớ vì nó chỉ lưu trữ các chuỗi con khớp với chuỗi hiện tại và tất cả các chuỗi gặp phải trước đó. Khi một hàng mới gọi UDA, mọi chuỗi con không tìm thấy trong chuỗi mới sẽ bị xóa khỏi bộ sưu tập.

Nó cũng hiệu quả với CPU ở chỗ, nếu tại bất kỳ thời điểm nào, số lượng các chuỗi con "phổ biến" bằng 0, nó sẽ đặt một cờ cho biết rằng không có chuỗi con nào có thể xảy ra và tất cả các lệnh thực thi trong tương lai sẽ thoát ra khi được gọi. Điều này xảy ra ngay lập tức nếu gặp một chuỗi trống. Trong bất kỳ trường hợp nào, một tài liệu XML trống (chỉ phần tử gốc) được trả về. Ý nghĩa của tài liệu trống tương đương với một chuỗi rỗng, vì điều duy nhất các chuỗi đầu vào có điểm chung là chúng không phải là NULLchuỗi.

NULL, theo cách giải thích của tôi, bị bỏ qua và không cho biết không có kết quả khớp nào giống như một chuỗi rỗng.

A NULLđược trả về trong hai trường hợp sau:

  • Tất cả các đầu vào là NULL
  • Chỉ có một hàng duy nhất NULLtrong tập hợp và do đó không có chuỗi nào khác để so sánh, do đó không có gì được coi là "phổ biến".

Tôi đã thêm một tham số đầu vào thứ hai để kiểm soát xem trả về chỉ là các chuỗi con chung dài nhất hoặc tất cả các chuỗi con chung. Trong trường hợp trả lại tất cả, một thuộc tính được thêm vào từng "mục" để cho biết liệu đó có phải là một trong các chuỗi con "dài nhất" hay không:

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test1a]
FROM   (VALUES (N'ABAB'), (N'BABA')) tab(col);

Trả về:

<Items Merged="False">
  <Item>ABA</Item>
  <Item>BAB</Item>
</Items>

SELECT dbo.LongestCommonSubstring(tab.col, 1) AS [Test1b]
FROM   (VALUES (N'ABAB'), (N'BABA')) tab(col);

Trả về:

<Items Merged="False">
  <Item IsLongest="True">ABA</Item>
  <Item IsLongest="True">BAB</Item>
  <Item IsLongest="False">AB</Item>
  <Item IsLongest="False">BA</Item>
  <Item IsLongest="False">A</Item>
  <Item IsLongest="False">B</Item>
</Items>

Ngoài ra, các so sánh hiện là trường hợp InSensitive để khớp với Collation điển hình.

Dưới đây là 16 trường hợp kiểm tra nữa chỉ kiểm tra chức năng, không phải hiệu suất. Tôi sẽ đăng các bài kiểm tra bổ sung sau đó hoạt động trên nhiều hàng chuỗi dài hơn nhiều. Bây giờ tôi cố tình bỏ đi Kết hợp các Nhân vật và Nhân vật Bổ trợ, vì chúng hơi phức tạp hơn một chút.

SELECT dbo.LongestCommonSubstring(tab.col, 1) AS [Test2]
FROM   (VALUES (N'ABAB'), (N'BABA'), (N'2BAB5')) tab(col);
-- <Items><Item>BAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test3]
FROM   (VALUES (N'ABAB'), (N'BABA'), (NULL), (N'2BAB5')) tab(col);
-- <Items><Item>BAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test4]
FROM   (VALUES (NULL), (NULL), (NULL)) tab(col);
-- NULL

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test5]
FROM   (VALUES (N'ABAB'), (N'BABA'), (N''), (N'2BAB5')) tab(col);
-- <Items />

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test6]
FROM   (VALUES (N'ABAB'), (N'BABA'), (N'L'), (N'2BAB5')) tab(col);
-- <Items />

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test7]
FROM   (VALUES (N'ABAB')) tab(col);
-- NULL

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test8a-DuplicatesAcross2Rows]
FROM   (VALUES (N'ABAB'), (N'ABAB')) tab(col);
-- <Items><Item>ABAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test8b-DuplicatesAcross3Rows]
FROM   (VALUES (N'ABAB'), (N'ABAB'), (N'ABAB')) tab(col);
-- <Items><Item>ABAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test8c-DuplicatesAcross4Rows]
FROM   (VALUES (N'ABAB'), (N'ABAB'), (N'ABAB'), (N'ABAB')) tab(col);
-- <Items Merged="False"><Item>ABAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test9-DuplicatesWithinOneString]
FROM   (VALUES (N'ABAB'), (N'zABABh2348923ABABf')) tab(col);
-- <Items Merged="False"><Item>ABAB</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test10-XmlEncodableCharacters]
FROM   (VALUES (N'ABA&B'), (N'zABA&Bh2348923ABA&Bf')) tab(col);
-- <Items Merged="False"><Item>ABA&amp;B</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test11a-FinalMatchesShorterThanInitialSet]
FROM   (VALUES (N'ABCDq1234g'), (N'1234qABCDg'), (N'uiyuiuy1234qBCDg'), (N'512tttrtrtBCDdfdfgdg')) tab(col);
-- <Items Merged="False"><Item>BCD</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test11b-FinalMatchesShorterThanInitialSet]
FROM   (VALUES (N'BCDq1234g'), (N'1234qABCDg'), (N'uiyuiuy1234qBCDg'), (N'512tttrtrtBCDdfdfgdg')) tab(col);
-- <Items Merged="False"><Item>BCD</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test11c-FinalMatchesShorterThanInitialSet]
FROM   (VALUES (N'ABCDq1234g'), (N'1234qABCDg'), (N'uiyuiuy1234qBCDg'), (N'5123tttrtrtBCDdfdfgdg')) tab(col);
-- <Items Merged="False"><Item>BCD</Item><Item>123</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test11d-FinalMatchesShorterThanInitialSet]
FROM   (VALUES (N'BCDq1234g'), (N'1234qABCDg'), (N'uiyuiuy1234qBCDg'), (N'5123tttrtrtBCDdfdfgdg')) tab(col);
-- <Items Merged="False"><Item>BCD</Item><Item>123</Item></Items>

SELECT dbo.LongestCommonSubstring(tab.col, 0) AS [Test11e-FinalMatchesShorterThanInitialSet]
FROM   (VALUES (N'BCDq1234g'), (N'1234qABCDg'), (N'uiyuiuy1234qBCDg'), (N'123tttrtrtBCDdfdfgdg')) tab(col);
-- <Items Merged="False"><Item>BCD</Item><Item>123</Item></Items>


SELECT dbo.LongestCommonSubstring(tab.col, 1) AS [Test12-CaseInSensivity]
FROM   (VALUES (N'AbAB'), (N'BAbA')) tab(col);
/*
<Items Merged="False">
  <Item IsLongest="True">AbA</Item>
  <Item IsLongest="True">bAB</Item>
  <Item IsLongest="False">Ab</Item>
  <Item IsLongest="False">bA</Item>
  <Item IsLongest="False">A</Item>
  <Item IsLongest="False">b</Item>
</Items>
*/

Cập nhật cuối cùng (hy vọng)

Tôi đã thực hiện một vài thay đổi nhỏ đối với mã kiểm tra được cung cấp trong câu trả lời của @ MisterMagoo để nó: a) trả lại các mối quan hệ cho chuỗi con chung "dài nhất" và b) bao gồm ký tự cuối cùng trong mỗi chuỗi như một phần của tìm kiếm. Tôi cũng đã thay đổi, một chút, làm thế nào các hàng thử nghiệm ban đầu được tập hợp sao cho chỉ có hơn 1 triệu hàng và chạy lại các thử nghiệm. Kết quả là phiên bản T-SQL mất 1 phút và 13 giây trong khi UDA SQLCLR chỉ mất 12 giây (và thậm chí có tùy chọn trả về tất cả các chuỗi con chung, không chỉ dài nhất).

Sau đó, tôi đã sửa đổi dữ liệu thử nghiệm để bao gồm một chuỗi con chung khác có cùng độ dài với người chiến thắng hiện tại (7 ký tự), một chuỗi con ngắn hơn nhưng vẫn phổ biến gồm 4 ký tự và 3 ký tự ngẫu nhiên. Điều này đã tăng kích thước chuỗi thử nghiệm tối đa từ 32 ký tự lên 46 ký tự. Chạy thử nghiệm lại (cùng mã), phiên bản T-SQL phải bị giết sau 23 phút và chỉ thử nghiệm 3 độ dài đầu tiên: 27, 26 và 25. UDA SQLCLR quay trở lại sau khoảng 1 phút 10 giây.

Đã đến lúc ăn mừng? SQLCLR đã lưu trong ngày chưa? Giữ lấy..

Trong một linh cảm, tôi quyết định xem liệu tối ưu hóa mà tôi đã thêm vào phiên bản SQLCLR có giúp ích cho T-SQL hay không, cụ thể là:

  • Lấy hai chuỗi ngắn nhất (ngắn nhất sẽ có số lượng chuỗi con ít nhất có thể và dù sao cũng sẽ là chuỗi dài nhất có thể).

  • Trích xuất tất cả các chuỗi con phổ biến có thể có từ hai chuỗi ngắn (bất kỳ chuỗi con nào là "chung" trên tất cả các hàng nhất thiết phải có trong tập hợp xuất phát từ chỉ hai chuỗi này và không có chuỗi con mới nào từ các hàng khác có thể được giới thiệu vì chúng sẽ không được giới thiệu "chung").

  • Bắt đầu với chuỗi con chung dài nhất, kiểm tra xem nó có được tìm thấy trong tất cả các hàng không. Tôi đã sử dụng IF (NOT EXISTS (WHERE CHARINDEX(substring, test_row) > 0))EXISTSmệnh đề sẽ thoát trên hàng đầu tiên trả về 0 (có nghĩa là chuỗi con không có) và do đó không cần phải kiểm tra tất cả các hàng. Phần này được thực hiện với một CURSOR(shh, đừng nói cho ai biết) vì nó cho phép bắt đầu ở đầu danh sách và chỉ cần chọn từng hàng mới mà không cần quét lại danh sách mỗi lần để tìm mục tiếp theo.

  • Khi tìm thấy chuỗi con đầu tiên, hãy lưu độ dài của nó trong một biến và lưu chính chuỗi con đó vào một biến bảng.

  • Tiếp tục kiểm tra, nhưng chỉ đối với các chuỗi có cùng độ dài với chuỗi con chung đầu tiên được tìm thấy. ngay khi độ dài của chuỗi con tiếp theo cần tìm nhỏ hơn độ dài của chuỗi con đầu tiên để khớp, thoát khỏi vòng lặp và dọn sạch con trỏ.

Tất cả những thứ con trỏ này phải làm cho nó chậm một cách đau đớn, phải không? Chà, hãy nhớ rằng phiên bản T-SQL trước đó sẽ mất vài giờ để hoàn thành và UDA SQLCLR mất 1 phút 10 giây, bạn có thể đoán phiên bản cập nhật này sẽ mất gì? Một vài phút? 10 phút? Hơn? Rốt cuộc, chỉ cần nhắc đến "con trỏ" là một cú đánh tự động trong 5 phút, phải không? Thời gian thực tế cho phiên bản sửa đổi:

22 giây !!!

Tất nhiên, điều đó phần lớn là do các chuỗi con nằm ở phía dài hơn (so với kích thước của chuỗi) và do đó vòng lặp có thể thoát ra sớm hơn nếu chuỗi con chung dài nhất chỉ dài 3 hoặc 4 ký tự (nghĩa là nó có ít để kiểm tra, nhưng đó vẫn là một trường hợp hợp lệ và không gian lận). Ngoài ra, một lợi ích khi làm việc trong T-SQL là bạn có thể kiểm tra toàn bộ tập hợp theo một giá trị duy nhất, trong khi SQLCLR chỉ có hàng hiện tại và không thể thấy toàn bộ tập hợp, do đó SQLCLR không thể đoản mạch khi tìm chuỗi con chung dài nhất (bởi vì nó sẽ không biết cái gì là "phổ biến" ở nơi đầu tiên cho đến khi nó được thực thi trên tất cả các hàng).

Một thay đổi cuối cùng là cho phép phiên bản T-SQL mới trả về tất cả các chuỗi con "chung", không chỉ các chuỗi con dài nhất, trong khi chỉ ra phiên bản nào dài nhất trong cột kiểu dữ liệu thứ hai BIT. Khi trả về tất cả các chuỗi con, phản ánh chức năng của UDA SQLCLR (ngay cả khi UDA chỉ trả về các chuỗi con chung dài nhất, nó vẫn có danh sách đầy đủ tất cả các chuỗi con được lưu trữ vì nó, một lần nữa, không có khả năng ngắn mạch), Phiên bản T-SQL trả về sau 2 phút 41 giây.

Vì vậy, có những điều kiện là T-SQL có thể nhanh hơn, thậm chí ở mức 1,2 triệu hàng. Nhưng hiệu suất ổn định hơn nhiều đối với phiên bản SQLCLR và chắc chắn nhanh hơn khi bạn muốn tất cả các chuỗi con phổ biến.

Kịch bản thử nghiệm, chứa cả 3 thử nghiệm cùng với kết quả của chúng, có thể được tìm thấy trên Pastebin tại:

SQLCLR UDA cho chuỗi con chung dài nhất - Kiểm tra

PS Mặc dù thử nghiệm đã được thực hiện trên 1,2 triệu hàng, chuỗi dài nhất được thử nghiệm là 46 ký tự. Tôi không chắc hiệu suất của một trong hai cách tiếp cận sẽ bị ảnh hưởng như thế nào khi hoạt động trên các chuỗi dài hơn nhiều. Việc kiểm tra đó sẽ phải đợi cho đến khi có thời gian để thực hiện ;-).


4

Solomon có thể đúng, nhưng cho đến khi giải pháp CLR xuất hiện, đây là phiên bản T-SQL để chơi.

/* Create some test data : on SQL 2016 this creates about 470K rows to test (with duplicates) */

 if object_id('tempdb..#Strings') is not null  drop table #Strings

   select a.Name as String, len(a.Name)+1 as StringLength
   into #Strings
   from sys.all_columns a, sys.all_columns b
   where a.Name like '%refer%';

 set nocount on;

 /* Any nulls mean there is not a "longest common string" */
 if exists(select 1 from #Strings where String is null)
 begin
   return;
 end;

/* We need to know number of rows in the sample and the length of the shortest string
   as the longest common substring cannot be longer than the shortest string in the set */

 declare @totalrows int;
 declare @minlen tinyint;
 declare @result varchar(50);

 select @minlen = min(StringLength-1), @totalrows = count(distinct String) from #Strings;

 raiserror(N'Maximum Possible Length: %d Total Distinct Rows: %d',0,0,@minlen,@totalrows) with nowait;

/* Check backwards from the longest possible string to the shortest and break when we find a match */
/* You might want to check the air conditioner is switched on here */

 while @minlen>1 and @result is null
 begin
   raiserror(N'Processing strings of length: %d',0,0,@minlen) with nowait;

   /* this method is "brute force" 
      1. find all substrings for each input string
      2. pick the first substring that appears in every input string
         we find this by grouping, counting and comparing to the number of input strings
   */
   select top(1) @result=match
   from (
     select String, substring(String, T.N, @minlen) match
     from #Strings
     cross apply ( select StringLength - @minlen ) a(L)
     cross apply (
       select top(a.L) V.N
       from (
         values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16),(17),(18),(19),(20),(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),(36),(37),(38),(39),(40),(41),(42),(43),(44),(45),(46),(47),(48),(49),(50)
         ) V(N)
       ) T(N)
     ) matches(String, match)
   group by match
   having count(distinct String) = @totalrows;
--   order by match;

   /* Decrement so next time we check for a shorter match */
   set @minlen = @minlen -1;
 end

/* display the result */
select 'Longest Common Substring: '+isnull(@result,'*** no match found ***');

3

CẬP NHẬT vào 20171127 lúc 11:58 PM CST ĐỂ XỬ LÝ UNIGRAM

Tôi biết rằng tôi đến hơi muộn ở đây (hơn một năm) nhưng, chức năng Chuỗi con chung dài nhất mới nhất của tôi nhanh hơn hàng nghìn lần so với bất cứ điều gì tôi thấy được đăng ở bất cứ đâu, kể cả CLR đã nói ở trên (tôi vừa thử nghiệm nó ).

Đó là một kỹ thuật bán vũ phu tôi đã đưa ra rằng:

  1. Phá vỡ ngắn hơn của hai chuỗi và tìm kiếm chuỗi con dài hơn cho một trận đấu.

  2. Chấp nhận tham số thứ 3 gọi là "cửa sổ" (nó giống tham số NTile hơn nhưng tôi sử dụng thuật ngữ "cửa sổ" vì ít người hiểu Ntile.) Đây là loại nước sốt bí mật khiến chú chó xấu này nhanh đến vậy

  3. Thường trình sau đó sử dụng một lực lượng thuần túy để xem liệu các chuỗi con có kích thước 20 hoặc ít hơn trong chuỗi ngắn cũng tồn tại trong chuỗi dài hơn. Sử dụng bảng kiểm đếm - cách tiếp cận lực lượng vũ trang cho kích thước 20 hoặc ít hơn sẽ là tức thời (0 ms).

  4. Sau đó, nó tìm kiếm chuỗi dài hơn cho các chuỗi con tồn tại trong chuỗi ngắn hơn có kích thước chia hết cho @window. Ví dụ: nếu @window = 100, nó sẽ tìm kiếm các chuỗi con phù hợp có độ dài 100, 200 .... cho đến độ dài của chuỗi dài hơn (ví dụ: nếu chuỗi dài hơn 515 ký tự, nó sẽ tìm kiếm khớp các chuỗi con dài 100, 200, 300, 400 và 500 ký tự.

  5. Khi chúng tôi đã xác định được "cửa sổ" chuỗi con nào dài nhất tồn tại, tôi sử dụng bảng kiểm đếm và mẹo "khoảng trống & đảo trên chuỗi" mà tôi đã học được từ Chris Morris (thảo luận ở đây ) để so sánh từng chuỗi cho các chuỗi con phù hợp.

  6. TOP 1 với các mối quan hệ được sử dụng để xác định chuỗi con dài nhất.

Chức năng):

CREATE FUNCTION dbo.getshortstring8k(@s1 varchar(8000), @s2 varchar(8000))
RETURNS TABLE WITH SCHEMABINDING AS RETURN 
SELECT
  s1 = CASE WHEN LEN(@s1) < LEN(@s2) THEN @s1 ELSE @s2 END,
  s2 = CASE WHEN LEN(@s1) < LEN(@s2) THEN @s2 ELSE @s1 END;
GO

CREATE FUNCTION dbo.lcssWindowAB(@s1 varchar(8000), @s2 varchar(8000), @window int)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
/*****************************************************************************************
Purpose
 Calculates the longest common substring between two varchar(n) strings up to 8000 
 characters each.

Developer Notes:
 1. Optimal performance gains will be seen on longer strings. All Longest Common Substring
    functions I have seen in SQL Server begin to choke at 500-1000. Set based Brute force 
    solutions that use a tally table are very fast for up to 1000 characters but begin to 
    slow dramatically after. 

    With N as the length of a string, The number of substrings is: N(N+1)/2
    For 1,000 character string: 1000(1000+1)/2 = 500,500   substrings
    For 2,000 characters:       2000(2000+1)/2 = 2,001,000 substrings

 2. Requires a materialized tally table beginning with 1 containing at least 8000 numbers;
    as written, this function will slow to a crawl using a cte tally table. This is due to
    the WHERE x.n BETWEEN 2 AND 10 clause. A CTE tally table will struggle with this but a
    correctly indexed materialized tally table will not. 

    For optimal performance your tally table should have a unique nonclustered index.
    THE DDL TO BUILD THE REQUIRED TALLY TABLE IS BELOW.

 3. Performance optimizations:

   3.1. The first major optimization is that the *shorter* of the two strings is broken up 
        into substrings which are then searched for in the larger string using CHARINDEX. 
        This reduces the number of substrings generated by:
          abs(len(@s1)-len(@s2)) * (abs(len(@s1)-len(@s2)) + 1) / 2  

        For example, if one is 20 characters longer than the other, 210 fewer substrings 
        will be evaluated; for 100 it's 5,050 fewer substrings, etc. 

   3.2. The second optimization is a technique I developed that I call "windowing". I use
        a @window parameter to break up the search specific windows for the presense of a
        matching substring. 1-10 legnth substrings in the smaller string are searched for 
        in the longer string. After that, only tokens with sizes evenly divisible by the
        @window parameter are evaluated. For example, say the short string length is 100
        and I set @window 20. All tokens sized 1-10 are searched for, the 20-grams, then
        40, 60... 100. This reduces the number of substrings from 5,050 to somewhere 
        beween 480 & 500 (depending on the size of the longest common substring.)

 4. The window parameter is for optimization only! It does not affect the final result set
    in any way. I strongly suggest that testing the function using different window sizes
    for different scenarios; the optimal size will vary. I have seen queries execute 3-10 
    faster when changing the window size. Start with 100, then try windows sizes of 
    20, 200, 300, 1000... 

 5. This function does not see a performance gain when run in parallel; 
    use option (maxdop 1) unless you're testing shows performance gains when running under 
    a parallel execution plan. 

Required Tally Table DDL (this will build exactly 8000 rows):
------------------------------------------------------------------------------------------
  -- (1) Create tally table
  IF OBJECT_ID('dbo.tally') IS NOT NULL DROP TABLE dbo.tally;
  CREATE TABLE dbo.tally (N int not null);

  -- (2) Add numbers to the tally table
  WITH DummyRows(V) AS ( 
    SELECT 1 FROM (VALUES -- 100 Dummy Rows
     ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),
     ($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($),($)) t(N))
  --INSERT dbo.tally
  SELECT TOP (8000) ROW_NUMBER() OVER (ORDER BY (SELECT 1))
  FROM DummyRows a CROSS JOIN DummyRows b CROSS JOIN DummyRows c;

  -- (3) Required constraints (and indexes) for performance
  ALTER TABLE dbo.tally ADD CONSTRAINT pk_cl_tally PRIMARY KEY CLUSTERED(N) 
    WITH FILLFACTOR = 100;

  ALTER TABLE dbo.tally ADD CONSTRAINT uq_nc_tally UNIQUE NONCLUSTERED(N);
  GO
------------------------------------------------------------------------------------------
Usage Examples:
  SELECT * FROM dbo.lcssWindowAB('abcxxx', 'yyyabczzz', 10);

  SELECT * FROM dbo.lcssWindowAB('123xxx', '!!123!!xxx!!', 10);

History:
 20171126 - Initial Development - Developed by Alan Burstein  
 20171127 - updated to return unigrams when longest common substring is 1;
            updated to use a materialized tally table; -- Alan Burstein
*****************************************************************************************/
SELECT TOP (1) WITH TIES itemIndex, itemLen = itemLen+addThis, item
FROM dbo.getshortstring8k(@s1, @s2) xs
CROSS APPLY
(
  SELECT
    itemIndex = MIN(position) over (partition by grouper order by (select $)),
    itemLen   = itemLen,
    addThis   = position-MIN(position) over (partition by grouper order by (select $)),
    item      = SUBSTRING
                (
                 xs.s1, 
                 MIN(position) over (partition by grouper order by (select $)),
                 itemLen+position-MIN(position) over (partition by grouper order by (select $))
                )
  FROM 
  (
    SELECT position - ROW_NUMBER() OVER (ORDER BY position), position, itemLen
    FROM
    (
      SELECT TOP (1) WITH TIES t.N, x.N -- Get the "longest" (including ties)
      FROM dbo.tally t                  -- all positions within the shorter string (s.s1)
      CROSS JOIN dbo.tally x            -- all sizes of substrings within the shorter string
      WHERE (t.N <= LEN(xs.s1) AND x.N <= LEN(xs.s1) AND LEN(xs.s1) - t.N + 1 - x.N >= 0)
      AND   (x.N BETWEEN 2 AND 10 OR x.N % @window = 0)      -- only 2-20 & @window-sized tokens
      AND   CHARINDEX(SUBSTRING(xs.s1, t.N, x.N), xs.s2) > 0 -- only tokens matched in both strings
      ORDER BY -x.N
    ) longSubstrings (position, itemLen)
    UNION ALL
    SELECT -position-1, position, 1
    FROM
    (
      SELECT t.N, 1             -- unigrams only
      FROM dbo.tally t          -- all positions within the shorter string (s.s1)
      WHERE (t.N <= LEN(xs.s1)) -- all valid unigrams
      AND   CHARINDEX(SUBSTRING(xs.s1, t.N, 1), xs.s2) > 0 -- only unigrams matched in both strings
    ) unigrams (position, itemLen)
  ) addGrouper (grouper, position, itemLen)
) lcssWindow (itemIndex, itemLen, addthis, item)
WHERE @window >= 10 AND @window%10 = 0 -- must be greater than 10 and divisible by 10
AND   CHARINDEX(item, xs.s2) > 0
ORDER BY -itemLen, -addThis;
GO

Đây là một ví dụ về cách hàm tính toán chuỗi con chung dài nhất giữa hai chuỗi dài khoảng 7700 ký tự. Nó trả về câu trả lời đúng trong 16 mili giây.

set statistics time on;

declare
  @s1 varchar(8000) = replicate('x',50)+replicate('Wow!',1900)+'!'+'junk,junk,junk...',
  @s2 varchar(8000) = replicate('z',95)+replicate('Wow!',1900)+'!'+'zzzzzzzzzzz......',
  @window int       = 100;

select len(@s1), len(@s2);

select * from dbo.lcssWindowAB(@s1, @s2, 100);
set statistics time off;

Đây là một phần của bài viết tôi đang làm việc. Nhiều hơn nữa


Xin chào Alan. Cảm ơn sự đóng góp này. Có vẻ rất thú vị. Tôi đã thử nghiệm nhanh và trong khi nó nhanh hơn, có một số điều cần lưu ý: a) khi chuỗi con chung dài nhất (LCS) ngắn hơn nhiều, thì chênh lệch thời gian giữa phương pháp này và phương pháp của tôi nhỏ hơn nhiều, gần hơn với 3,5 x ở cấp thấp, b) vì một số lý do, chức năng của bạn sẽ không trả về kết quả nếu LCS chỉ có 1 ký tự, c) SQLCLR chỉ hoạt động NVARCHARtrong khi điều này VARCHARd) điều này không hoạt động trên một tập hợp các hàng , đó là yêu cầu ở đây. Bạn có thể vui lòng đặt nó dựa trên thiết lập và sửa mục b không?
Solomon Rutzky

Solomon - Tôi đã quên đề cập rằng nó không tính toán lcs khi nó là một unigram; điều này có thể dễ dàng sửa chữa mà không cần một hiệu suất, tôi chỉ quyết định làm thế nào để làm điều đó. Một lần nữa - tôi sẽ đề cập đến điều đó và quên đi. Hiệu suất sẽ thay đổi dựa trên các kịch bản khác nhau - Tôi đã thử nghiệm điều này trong một vài tháng. Tôi sẽ cố gắng thực hiện các cập nhật khác mà bạn đề cập và đăng chúng sau hôm nay.
Alan Burstein

Âm thanh tốt. Chỉ cần cố gắng tiến gần đến một so sánh táo với táo như chúng ta có thể nhận được, đặc biệt là vì những cân nhắc nhất định đã được thực hiện trong cả cách tiếp cận SQLCLR của tôi và phương pháp T-SQL của MisterMagoo để giải thích cho việc làm việc trên các hàng có thể nghe được. Cách tiếp cận của bạn có thể vẫn là nhanh nhất, tôi chỉ muốn chắc chắn rằng không có gì sai lệch, hoặc có khả năng gây hiểu lầm, trong so sánh.
Solomon Rutzky

Này Solomon - Tôi đã cập nhật chức năng (ở trên). Nó hoạt động trên unigram bây giờ. Tôi chỉ đăng nó để bạn có thể có một cái nhìn. Giải thích thêm trong các ý kiến. Tôi đã bắt đầu với phiên bản NVARCHAR (4000) nhưng bị kẹt ở thứ gì đó - tôi sẽ xem lại vào ngày mai. Tôi cũng sẽ thêm mã để đưa ra kết quả theo định dạng bạn cần vào ngày hôm sau. Chúc mừng!
Alan Burstein
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.