Использование JSON для матриц в SQL Server
Начиная с SQL Server 2017, использование массивов JSON для представления и обработки матрицы становится более практичным. SQL Server может читать их и обновлять значения в них, но не может их создавать. Для этого вам нужно создать JSON в виде строки. Большим их преимуществом является то, что вы можете передавать их между процедурами и функциями так же легко, как и любые другие строки, и быстро превращать их в таблицы.
Для разработчика SQL Server, матрицы, вероятно, наиболее полезны для решения более сложных задач поиска строк с использованием динамического программирования. Как только вы освоите этот тип техники, ряд, казалось бы, неразрешимых проблем значительно уменьшится. Есть около пятидесяти общих проблем со структурой данных, которые можно решить с помощью динамического программирования. До SQL Server 2017 это было трудно сделать в SQL из-за отсутствия поддержки этого стиля программирования. Запоминание, один из принципов этой техники, легко реализовать в SQL, но очень сложно преобразовать существующие процедурные алгоритмы для использования табличных переменных. Обычно проще и быстрее использовать строки в качестве псевдопеременных, как мы это делали с Edit Distance и алгоритмом Левенштейна, самой длинной общей подпоследовательностью и самой длинной общей подстрокой. Проблема с этим заключается в том, что код для извлечения значений массива может быть очень трудно расшифровать или отладить. JSON может сделать это очень легко с помощью ссылок на массивы путей.
Можно ли будет использовать массивы JSON для решения одной из этих проблем? Если так, намного ли это медленнее? Мы подумали, что было бы интересно преобразовать проблему самой низкой общей подпоследовательности в форму, основанную на json, и выполнить несколько тестов подряд. Для тех, кто имел привычку TLDR, был сделан вывод, что для его запуска требуется в два-три раза больше времени, но получается код, который легче писать, понимать и отлаживать. Мы подозреваем, что есть способы и средства сделать это быстрее.
.
IF Object_Id(N'LCS') IS NOT NULL DROP FUNCTION LCS; GO CREATE FUNCTION LCS /** summary: > The longest common subsequence (LCS) problem is the problem of finding the longest subsequence common to all sequences in two sequences. It differs from problems of finding common substrings: unlike substrings, subsequences are not required to occupy consecutive positions within the original sequences. For example, the sequences "1234" and "1224533324" have an LCS of "1234": Author: Phil Factor Revision: 1.0 date: 05 April 2019 example: code: | Select dbo.lcs ('1234', '1224533324') Select dbo.lcs ('thisisatest', 'testing123testing') Select dbo.lcs ( 'XMJYAUZ', 'MZJAWXU') Select dbo.lcs ( 'beginning-middle-ending', 'beginning-diddle-dum-ending') returns: > the longest common subsequence as a string **/ (@xString VARCHAR(MAX), @yString VARCHAR(MAX)) RETURNS VARCHAR(MAX) AS BEGIN
DECLARE @ii INT = 1; --inner index DECLARE @jj INT = 1; --next loop index DECLARE @West INT; --array reference number to left DECLARE @NorthWest INT; --array reference previous left DECLARE @North INT; --array reference previous DECLARE @Max INT; --holds the maximum of two values DECLARE @Current INT; --current number of matches DECLARE @Matrix NVARCHAR(MAX); DECLARE @PreviousRow NVARCHAR(2000); -- the previous matrix row DECLARE @JSON NVARCHAR(4000); --json work variable DECLARE @Numbers TABLE (jj INT); -- SQL Prompt formatting off INSERT INTO @numbers(jj) --this is designed for words of max 40 characters 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) -- SQL Prompt formatting on --the to start with, the first row is all zeros. SELECT @PreviousRow = N'[' + Replicate('0,', Len(@xString) + 1) + N'"' + Substring(@yString, 1, 1) + N'"]'; SELECT @Matrix = @PreviousRow;--add this to the matrix /* we now build the matrix in bottom up fashion. */ WHILE (@ii <= Len(@yString)) BEGIN SELECT @West = 0, @JSON = NULL; --now create a row in just one query SELECT @NorthWest = Json_Value(@PreviousRow, '$[' + Cast(jj - 1 AS VARCHAR(5)) + ']'), @North = Json_Value(@PreviousRow, '$[' + Cast(jj AS VARCHAR(5)) + ']'), @Max = CASE WHEN @West > @North THEN @West ELSE @North END, @Current = CASE WHEN Substring(@xString, jj, 1) = Substring(@yString, @ii, 1) THEN @NorthWest + 1 ELSE @Max END, @JSON = Coalesce(@JSON + ',', '[0,') + Coalesce(Cast(@Current AS VARCHAR(5)), 'null'), @West = @Current FROM @Numbers AS f WHERE f.jj <= Len(@xString); --and store the result as the previous row SELECT @PreviousRow = @JSON + N',"' + Substring(@yString, @ii, 1) + N'"]'; --and add the reow to the matrix SELECT @Matrix = Coalesce(@Matrix + ', ', '') + @PreviousRow, @ii = @ii + 1; END; --we add the boundong brackets. SELECT @Matrix = N'[' + @Matrix + N']'; SELECT @ii = Len(@yString), @jj = Len(@xString); DECLARE @previousColScore INT, @PreviousRowScore INT, @Ychar NCHAR; DECLARE @Subsequence NVARCHAR(4000) = ''; WHILE (@Current > 0) BEGIN SELECT @Ychar = Substring(@yString, @ii, 1); IF (@Ychar = Substring(@xString, @jj, 1)) -- If current character in X[] and Y[] are same, then it is part of LCS SELECT @ii = @ii - 1, @jj = @jj - 1, @Subsequence = @Ychar + @Subsequence, @Current = @Current - 1; ELSE --If not same, then find the larger of two and traverse in that direction BEGIN --find out the two scores, one to the north and one to the west SELECT @PreviousRowScore = Json_Value( @Matrix, 'strict $[' + Convert(VARCHAR(5), @ii - 1) + '][' + Convert(VARCHAR(5), @jj) + ']' ), @previousColScore = Json_Value( @Matrix, 'strict $[' + Convert(VARCHAR(5), @ii) + '][' + Convert(VARCHAR(5), @jj - 1) + ']' ); --either go north or west IF @PreviousRowScore < @previousColScore SELECT @jj = @jj - 1; ELSE SELECT @ii = @ii - 1; END; END; RETURN @Subsequence; END; GO -- Now we do a quick test and timing with the old version DECLARE @timing DATETIME; SELECT @timing = GetDate();
IF dbo.LongestCommonSubsequence('1234', '1224533324') <> '1234' RAISERROR('test 1 failed', 16, 1); IF dbo.LongestCommonSubsequence('thisisatest', 'testing123testing') <> 'tsitest' RAISERROR('test 2 failed', 16, 1); IF dbo.LongestCommonSubsequence('Patient', 'Complaint') <> 'Paint' RAISERROR('test 3 failed', 16, 1); IF dbo.LongestCommonSubsequence('XMJYAUZ', 'MZJAWXU') <> 'MJAU' RAISERROR('test 4 failed', 16, 1); IF dbo.LongestCommonSubsequence('yab', 'xabyrbyab') <> 'yab' RAISERROR( 'test 5 failed', 16, 1 ); IF dbo.LongestCommonSubsequence( 'beginning-middle-ending', 'beginning-diddle-dum-ending' ) <> 'beginning-iddle-ending' RAISERROR('test 6 failed', 16, 1);
SELECT DateDiff(MILLISECOND, @timing, GetDate()) AS [ms FOR traditional way]; --now do the same test run with the current function SELECT @timing = GetDate();
IF dbo.LCS('1234', '1224533324') <> '1234' RAISERROR('test 1 failed', 16, 1); IF dbo.LCS('thisisatest', 'testing123testing') <> 'tsitest' RAISERROR( 'test 2 failed', 16, 1 ); IF dbo.LCS('Patient', 'Complaint') <> 'Paint' RAISERROR('test 3 failed', 16, 1); IF dbo.LCS('XMJYAUZ', 'MZJAWXU') <> 'MJAU' RAISERROR('test 4 failed', 16, 1); IF dbo.LCS('yab', 'xabyrbyab') <> 'yab' RAISERROR('test 5 failed', 16, 1); IF dbo.LCS('beginning-middle-ending', 'beginning-diddle-dum-ending') <> 'beginning-iddle-ending' RAISERROR('test 6 failed', 16, 1);
SELECT DateDiff(MILLISECOND, @timing, GetDate()) AS [ms FOR JSON-based] ; |
Это возвращает ...
ms FOR traditional way ---------------------- 10
(1 row affected)
ms FOR JSON-based ----------------- 30
(1 row affected) |
Эти тесты являются частью сценария сборки для процедуры, которые помогают убедиться, что в ней осталось минимум ошибок!
Очевидно хотелось бы немного увеличить скорость запросов JSON, но это приемлемо, если не делать много подобных запросов. Я рад, что мы, JSON, сделали это, потому что быстрее наладить работу. В моей статье «Нечеткие поиски в SQL Server» я показываю, как можно сократить количество поисков, предварительно отфильтровав вероятных кандидатов с помощью обычных команд SQL.