Работает ли условие CROSS APPLY WHERE до перекрестного применения или после по результатам - PullRequest
2 голосов
/ 17 октября 2019

Я выполняю нестандартный алгоритм нечеткого сопоставления, который нам нужен в одном из наших внутренних приложений. Я пытаюсь ускорить это. Когда я делаю перекрестное наложение нечеткой функции, чтобы найти предложенные совпадения, я не хочу искать ненужные данные.

Вот функция:

select top 5 Manufacturer,ManufacturerPartNumber,Description as ManufacturerDescription, CONVERT(money,Price) as Price,fms.Score  
from Products_OurProducts_Products_View
    CROSS APPLY (
         select 
         dbo.FuzzyControlMatch(@objectModel, ManufacturerPartNumber) AS score
    ) AS fms

ORDER BY fms.Score DESC

Теперь скажите, что мой номер детали изготовителя, отправленный пользователем, - FD1234, нам не нужно запутывать ВСЕ номера производителей. Могу ли я добавить подобное предложение, чтобы получить более точный набор данных для нечеткого, или это произойдет ПОСЛЕ того, как перекрестное применение уже выполняется и повлияет только на конечный результат.

 select top 5 Manufacturer,ManufacturerPartNumber,Description as ManufacturerDescription, CONVERT(money,Price) as Price,fms.Score  
from Products_OurProducts_Products_View
    CROSS APPLY (
         select 
         dbo.FuzzyControlMatch(@objectModel, ManufacturerPartNumber) AS score
    ) AS fms
 WHERE LEN(ManufacturerPartNUmber) < LEN(@objectModel)+5

Я надеялся на перекрестное применениетолько нечётко к элементам, близким к длине параметра в @ objectmodel.

1 Ответ

1 голос
/ 18 октября 2019

Я надеялся, что кросс применим только к нечетким элементам close к длине параметра в @ objectmodel.

Для измерения близости используйте ABS (ab). Если вы хотите строки одинаковой (близкой) длины, это будет:

ABS(LEN(string1) - LEN(string2))

Если вы хотите строки, которые, скажем, не более чем на 5 символов длиннее / короче, чем другие, предложение WHERE будет выглядеть как

WHERE ABS(LEN(string1) - LEN(string2)) <= 5

Можно ли добавить подобное предложение, чтобы получить более точный набор данных для нечеткого, или это произойдет ПОСЛЕ того, как перекрестное применение уже выполняется и повлияет только на конечный результат.

Обработка логических запросов SQL Server (https://images.app.goo.gl/qTQpdg2NsC7M4eUj9) требует, чтобы предложение WHERE оценивалось вторым (после FROM). Скалярные UDF в предложениях WHERE изменяют это. dbo.FuzzyControlMatch - это скалярная UDF T-SQL, которая используется в предложении WHERE, затем сначала будет обработана, и вам придется все оценивать. Однако это не относится к тому, что вы опубликовали.

Как улучшить производительность?

Для начала я бы вычислил длину строки заранее. Вы можете использовать постоянный вычисляемый столбец, а затем объявление. d индекс ON (длина строки, вне зависимости от того, что ТОП 5 должен быть упорядочен). Затем включите другие столбцы, используемые этим запросом для столбцов INCLUDE.

В качестве альтернативы вы можете предварительно фильтровать, используя временную таблицу или табличную переменную, и вместо этого применять свою функцию там.

В любом случае, у меня есть функция и пара алгоритмов, которые изменят вашу жизнь, если вы будете часто сталкиваться со сходством строк. Я опубликую их позже сегодня вечером, когда у меня будет больше времени.

ПРОДОЛЖЕНИЕ ....

Я не знаком с вашей функцией подобия, поскольку вы не предоставили DDL, но я знаю, как измерить сходство строк https://itnext.io/string-similarity-the-basic-know-your-algorithms-guide-3de3d7346227 с использованием таких алгоритмов, как Левенштейн, Дамерау-Левенштейн, Самая длинная общая подстрока и Самая длинная общая подпоследовательность (LCSQ). Левенштейн может быть решен за O (m n) времени. Дамерау-Левенштейна в O (m n * (Max (m, n)) время. Самая длинная общая подстрока в O (n m) или O (n) + O (n k) собобщенное суффиксное дерево. Самая длинная общая подпоследовательность между двумя строками - NP-Hard.

Levenshtein и Damerau-Levenshtein являются метриками расстояния https://en.wikipedia.org/wiki/String_metric и могут измерять аналогично следующим образом: взять две строки S1и S2, S1 всегда короче или равны S2; L1 как длина S1, L2 длина S2 и E как ваше расстояние редактирования ... Формула для подобия между (S1, S2): (L1-E) / L2. Рассмотрим слова «Их» и «Тьер». Обе строки имеют длину 6 символов, а расстояние Левенштейна равно 2. (6-2) / 6 = 0,67;По словам Левенштейна, эти парни похожи на 67%. Расстояние Дамерау-Левенштейна между этими строками равно 1; (6-1) / 6 = 0,83; для ОДЗ эти строки имеют показатель сходства 83%.

С самой длинной общей подстрокой или подпоследовательностью (длина обоих как LS) сходство LS / L2. Например, самая длинная соподстрока mmon между 'ABC123' и "ABC12.3" является "ABC12";LS = 5, L2 = 7, 5/7 = 71% аналогично. Самая длинная общая подпоследовательность между ними - «ABC123». LS = 6, SELECT 6./7 = 86% сходства.

Представляем Расстояние Берни ;все просто: когда S1 является подстрокой S2, расстояние Берни (BD) равно L2-L1, и сходство можно измерить с помощью L1 / L2. Например: BD ("xxx", "yyy") возвращает NULL;BD («Кенгуру», «Кенгуру») = 1 ... L2 = 9, L1 = 8, L2-L1 = 1. Здесь Берни дает нам дистанцию ​​1 и оценку сходства SELECT .88 (88%). Эти две метрики вычисляются за очень короткое время - они в основном бесплатны. Bernie Mertics часто будет NULL;когда это так, вы ничего не потеряли и ничего не получили. Однако, когда Берни не NULL, вы только что достигли чего-то особенного .... * Вы решили Левенштейна (LD), Дамерау-Левенштейна (DLD), самого длинного общегоПодстрока и подпоследовательность (LCSS) и многое, многое другое. * Когда Берни (B) НЕ равен NULL, тогда LD = B, DLD = B и LCSS = L1. Я бы не удивился, если бы вы применили Берни к своей функции подобия ; ^) Это известно как уменьшение :

Включено вконец этого поста - bernie8K (VARCHAR (8000)). В дополнение к расстоянию Берни и Симилари вы можете использовать Берни для вычисления Максимальное сходство (MS). Пример: MS = L1 / L2. MS («ABCD», «ABCXYZ») составляет 67%. Другими словами, когда L1 = 4 и L2 = 6, две строки не могут быть более 67% (4/6 = 0,6666). Вооружившись этой информацией, вы можете создать параметр Minimum Similarity , который позволит вам значительно сократить количество сравнений. Теперь демо.

Проблема:

Когда-то у меня был крупный клиент с тысячами сотрудников. В БД они унаследовали сотни дубликатов должностей, которые были введены вручную, например, «Кредитный офицер» и «Кредитный офицер». Отчеты будут сообщать, что у них были кредитные офицеры 2005 года и 16 кредитных офицеров. В действительности у них было 2021 ссудных офицеров (16 с ошибочными названиями должностей). Задача состояла в том, чтобы идентифицировать (и обмануть) эти должности. Этот пример является уменьшенной версией проблемы. Обратите внимание на мои комментарии.

-- Sample data.
DECLARE @jobs TABLE
(
  JobId  INT IDENTITY PRIMARY KEY,
  JobCat VARCHAR(100) NOT NULL
);

INSERT @jobs(JobCat) 
VALUES('Editing Department'),('Director'),('Producer'),('Actor'),
      ('Film Editing Department'),('Producers'),('Directer');

-- without any pre-filtering I would need to compare 21 pairs of strings "strings pairs"....
SELECT      j1.JobCat, j2.JobCat
FROM        @jobs AS j1
CROSS JOIN  @jobs AS j2
CROSS APPLY samd.bernie8k(j1.JobCat, j2.JobCat) AS  b
WHERE       j1.JobId < j2.JobId;

Возвращает:

    JobCat                             JobCat
---------------------------------- ---------------------------------
Editing Department                 Director
Editing Department                 Producer
...
Director                           Directer
Producer                           Actor
...

Теперь мы используем расстояние Берни, чтобы получить наш ответ и исключить ненужные сравнения. Пары строк, где B не равен NULL, были решены, пары строк, в которых MS <@MinSim, были удалены. <em>Мы только что сократили нашу работу с 21 сравнения до 5 и очень быстро выявили 2 дубликата.

DECLARE @MinSim DEC(6,4) = .8;

SELECT      j1.JobId, j2.JobId, b.S1, b.S2, b.L1, b.L2, b.B, b.MS, b.S
FROM        @jobs AS j1
CROSS JOIN  @jobs AS j2
CROSS APPLY samd.bernie8k(j1.JobCat, j2.JobCat) AS  b
WHERE       j1.JobId < j2.JobId
AND         (b.MS >= @MinSim OR b.B IS NOT NULL);

Возвращает:

    JobId       JobId       S1                         S2                    L1   L2  B     MS       S       
----------- ----------- --------------------- -------------------------- ---- --- ----- -------- -------
1           5           Editing Department    Film Editing Department    18   23  5     0.7826   0.7826
2           3           Director              Producer                   8    8   NULL  1.0000   NULL
2           6           Director              Producers                  8    9   NULL  0.8889   NULL
2           7           Director              Directer                   8    8   NULL  1.0000   NULL
3           6           Producer              Producers                  8    9   1     0.8889   0.8889
3           7           Producer              Directer                   8    8   NULL  1.0000   NULL
6           7           Directer              Producers                  8    9   NULL  0.8889   NULL

ThisСокращение вещи это круто! Давайте принесем еще пару алгоритмов на вечеринку. Сначала мы возьмем копию ngrams8k и создадим функцию для вычисления расстояния Хэмминга для подобия. Хэмминга (HD) можно вычислить за O (n) время;сходство как (L1-HD) / L2. Обратите внимание, что когда HD = 1, то LD = 1, DLD = 1, LCSS = L1-1, и мы, вероятно, также вычислили ваше сходство.

-- Sample data.
DECLARE @jobs TABLE
(
  JobId  INT IDENTITY PRIMARY KEY,
  JobCat VARCHAR(100) NOT NULL
);

INSERT @jobs(JobCat) 
VALUES('Editing Department'),('Director'),('Producer'),('Actor'),
      ('Film Editing Department'),('Producers'),('Directer');

DECLARE @MinSim DECIMAL(6,4) = .8;

WITH br AS
(
  SELECT      b.*
  FROM        @jobs AS j1
  CROSS JOIN  @jobs AS j2
  CROSS APPLY samd.bernie8k(j1.JobCat, j2.JobCat) AS  b
  WHERE       j1.JobId < j2.JobId
  AND         (b.MS >= @MinSim OR b.B IS NOT NULL)
) 
SELECT      br.S1, br.S2, br.L1, br.L2, br.D, S = h.MinSim
FROM        br
CROSS APPLY samd.HammingDistance8k(br.S1, br.S2) AS h
WHERE       br.B IS NULL
AND         h.MinSim >= @MinSim
UNION ALL
SELECT      br.S1, br.S2, br.L1, br.L2, br.D, br.S
FROM        br
WHERE       br.B IS NOT NULL;

Возвращает:

S1                     S2                        L1          L2          D           S
---------------------- ------------------------- ----------- ----------- ----------- --------------
Director               Directer                  8           8           0           0.87500000000
Editing Department     Film Editing Department   18          23          5           0.78260000000
Producer               Producers                 8           9           1           0.88890000000

Резюме:

Мы начали с 21 пары строк для сравнения. Используя Берни, мы сократили это число до 5 (2 решено, 14 исключено). Используя Хемминга, мы выбрали другое. Осталось только четыре!

Функции:

CREATE FUNCTION samd.bernie8K
(
  @s1 VARCHAR(8000), 
  @s2 VARCHAR(8000)
)
/*****************************************************************************************
[Purpose]:
 This function allows developers to optimize and simplify how they fuzzy comparisons 
 between two strings (@s1 and @s2). 

 bernie8K returns:
  S1  = short string - LEN(S1) will always be <= LEN(S2); The formula to calculate S1 is:
          S1 = CASE WHEN LEN(@s1) > LEN(@s2) THEN @s2, ELSE @s1 END;
  S2  = long string  - LEN(S1) will always be <= LEN(S2); The formula to calculate S1 is:
          S2 = CASE WHEN LEN(@s1) > LEN(@s2) THEN @s1, ELSE @s2;
  L1  = short string length = LEN(S1)
  L2  = long string length  = LEN(S2)
  D   = distance            = L2-L1; how many characters needed to make L1=L2; D tells us:
          1. D    is the *minimum* Levenshtein distance between S1 and S2
          2. L2/D is the *maximum* similarity between S1 and S2
  I   = index               = CHARINDEX(S1,S2);
  B   = bernie distance     = When B is not NULL then:
          1. B = The Levenshtein Distance between S1 and S2
          2. B = The Damarau-Levenshtein Distance bewteen S1 and S2
          3. B = The Longest Common Substring & Longest Common Subsequence of S1 and S2
          4. KEY! = The similarity between L1 and L2 is L1/l2
  MS  = Max Similarity      = Maximum similarity
  S   = Minimum Similarity  = When B isn't null S is the same Similarity value returned by
        mdq.Similarity: https://msdn.microsoft.com/en-us/library/ee633878(v=sql.105).aspx

[Author]:
  Alan Burstein

[Compatibility]: 
 SQL Server 2005+, Azure SQL Database, Azure SQL Data Warehouse & Parallel Data Warehouse

[Parameters]:
 @s1 = varchar(8000); First of two input strings to be compared
 @s2 = varchar(8000); Second of two input strings to be compared

[Returns]:
 S1 = VARCHAR(8000); The shorter of @s1 and @s2; returns @s1 when LEN(@s1)=LEN(@s2)
 S2 = VARCHAR(8000); The longer  of @s1 and @s2; returns @s2 when LEN(@s1)=LEN(@s2)
 L1 = INT; The length of the shorter of @s1 and @s2 (or both when they're of equal length)
 L2 = INT; The length of the longer  of @s1 and @s2 (or both when they're of equal length)
 D  = INT; L2-L1; The "distance" between L1 and L2
 I  = INT; The location (position) of S1 inside S2; Note that when 1>0 then S1 is both:
       1.  a substring   of S2
       2.  a subsequence of S2
 B  = INT; The Bernie Distance between @s1 and @s1; When B is not null then:
       1. B = The Levenshtein Distance between S1 and S2
       2. B = The Damarau-Levenshtein Distance bewteen S1 and S2
       3. B = The Longest Common Substring & Longest Common Subsequence of S1 and S2
       4. KEY! = The similarity between L1 and L2 is L1/l2
 MS = DECIMAL(6,4); Returns the same simlarity score as mdq.Similarity would if S1 where a
      substring of S2
 S  = DECIMAL(6,4); When B isn't null then S is the same Similarity value returned by
      mdq.Similarity

 For more about mdq.Similarity visit:
    https://msdn.microsoft.com/en-us/library/ee633878(v=sql.105).aspx

[Syntax]:
--===== Autonomous
 SELECT b.TX, b.S1, b.S2, b.L1, b.L2, b.D, b.I, b.B, b.MS, b.S
 FROM   samd.bernie8K('abc123','abc12') AS b;

--===== CROSS APPLY example
 SELECT b.TX, b.S1, b.S2, b.L1, b.L2, b.D, b.I, b.B, b.MS, b.S
 FROM        dbo.SomeTable            AS t
 CROSS APPLY samd.bernie8K(t.S1,t.S2) AS b;

[Dependencies]:
 N/A

[Developer Notes]:
 X. Bernie ignores leading and trailing spaces trailing, and returns trimmed strings!
 1. When @s1 is NULL then S2 = @s2, L2 = LEN(@s2); 
    When @s2 is NULL then S1 = @s1, L1 = LEN(@s1)
 2. bernie8K ignores leading and trailing whitespace on both input strings (@s1 and @s2). 
    In other words LEN(@s1)=DATALENGTH(@s1), LEN(@s2)=DATALENGTH(@s2)
 3. bernie8K is deterministic; for more about deterministic and nondeterministic
    functions see https://msdn.microsoft.com/en-us/library/ms178091.aspx

[Examples]:
--==== 1. BASIC USE:
  -- 1.1. When b.I > 0  
  SELECT b.TX, b.S1, b.S2, b.L1, b.L2, b.D, b.I, b.B, b.MS, b.S
  FROM samd.bernie8K('abc1234','bc123') AS b;

  -- 1.2. When b.I = 0
  SELECT b.TX, b.S1, b.S2, b.L1, b.L2, b.D, b.I, b.B, b.MS, b.S
  FROM samd.bernie8K('abc123','xxx') AS b;
-----------------------------------------------------------------------------------------
[Revision History]:
 Rev 00 - 20180708 - Inital Creation - Alan Burstein
 Rev 01 - 20181231 - Added Boolean logic for transpositions (TX column) - Alan Burstein
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT 
  TX = base.TX,     -- transposed? boolean - were S1 and S2 transposed?
  S1 = base.S1,     -- short string >> IIF(LEN(@s1)>LEN(@s2),@s2,@s1)
  S2 = base.S2,     -- long  string >> IIF(LEN(@s1)>LEN(@s2),@s1,@s2)
  L1 = base.L1,     -- short string length >> IIF(LEN(@s1)>LEN(@s2),LEN(@s2),LEN(@s1))
  L2 = base.L2,     -- long  string length >> IIF(LEN(@s1)>LEN(@s2),LEN(@s1),LEN(@s2))
  D  = base.D,        -- bernie string distance >> # of characters needed to make L1=L2
  I  = iMatch.idx,  -- bernie index >> position of S1 within S2
  B  = bernie.D,      -- bernie distance >> IIF(CHARINDEX(S1,S2)>0,L2-L1,NULL)
  MS = maxSim.D,    -- maximum similarity
  S  = similarity.D -- (minimum) similarity
FROM
(
  SELECT
    TX = CASE WHEN ls.L=1 THEN 1 ELSE 0 END,
    S1 = CASE WHEN ls.L=1 THEN s.S2 ELSE s.S1 END,
    S2 = CASE WHEN ls.L=1 THEN s.S1 ELSE s.S2 END,
    L1 = CASE WHEN ls.L=1 THEN l.S2 ELSE l.S1 END,
    L2 = CASE WHEN ls.L=1 THEN l.S1 ELSE l.S2 END,
    D  = ABS(l.S1-l.S2)

  FROM        (VALUES(LEN(LTRIM(@s1)),LEN(LTRIM(@s2))))     AS l(S1,S2) -- LEN(S1,S2)
  CROSS APPLY (VALUES(RTRIM(LTRIM(@S1)),RTRIM(LTRIM(@S2)))) AS s(S1,S2) -- S1 and S2 trimmed
    CROSS APPLY (VALUES(SIGN(l.S1-l.S2)))                     AS ls(L)    -- LeftLength
) AS base
CROSS APPLY (VALUES(ABS(SIGN(base.L1)-1),ABS(SIGN(base.L2)-1)))             AS blank(S1,S2)
CROSS APPLY (VALUES(CHARINDEX(base.S1,base.S2)))                            AS iMatch(idx)
CROSS APPLY (VALUES(CASE WHEN SIGN(iMatch.idx|blank.S1)=1 THEN base.D END)) AS bernie(D)
CROSS APPLY (VALUES(CAST(CASE blank.S1 WHEN 1 THEN 1.*blank.S2 
                      ELSE 1.*base.L1/base.L2 END AS DECIMAL(6,4))))        AS maxSim(D)
CROSS APPLY (VALUES(CAST(1.*NULLIF(SIGN(iMatch.idx),0)*maxSim.D 
                      AS DECIMAL(6,4))))                                    AS similarity(D);
GO

CREATE FUNCTION dbo.rangeAB
(
  @low  BIGINT, -- (start) Lowest  number in the set
  @high BIGINT, -- (stop)  Highest number in the set
  @gap  BIGINT, -- (step)  Difference between each number in the set
  @row1 BIT     -- Base: 0 or 1; should RN begin with 0 or 1?
)
/****************************************************************************************
[Purpose]:
 Creates a lazy, in-memory...

[Author]: Alan Burstein

[Compatibility]: 
 SQL Server 2008+ and Azure SQL Database 

[Syntax]:
 SELECT r.RN, r.OP, r.N1, r.N2
 FROM   dbo.rangeAB(@low,@high,@gap,@row1) AS r;

[Parameters]:
 @low  = BIGINT; represents the lowest  value for N1.
 @high = BIGINT; represents the highest value for N1.
 @gap  = BIGINT; represents how much N1 and N2 will increase each row. @gap also
         represents the difference between N1 and N2.
 @row1 = BIT; represents the base (first) value of RN. When @row1 = 0, RN begins with 0,
         when @row = 1 then RN begins with 1.

[Returns]:
 Inline Table Valued Function returns:
 RN = BIGINT; a row number that works just like T-SQL ROW_NUMBER() except that it can 
      start at 0 or 1 which is dictated by @row1. If you are returning the numbers:
      (0 or 1) Through @high, then use RN as your "N" value, otherwise use N1.
 OP = BIGINT; returns the "opposite number" that relates to RN. When RN begins with 0 the
      first number in the set will be 0 for RN, the last number in will be 0 for OP. When
      RN is 1 to 10, the numbers 1 to 10 are retrurned in ascending order for RN and in
      descending order for OP. 

      Given the Numbers 1 to 3, 3 is the opposite of 1, 2 the opposite of 2, and 1 is the
      opposite of 3. Given the numbers -1 to 2, the opposite of -1 is 2, the opposite of 0
      is 1, and the opposite of 1 is 0.  
 N1 = BIGINT; This is the "N" in your tally table/numbers function. this is your *Lazy* 
      sequence of numbers starting at @low and incrimenting by @gap until the next number
      in the sequence is greater than @high.
 N2 = BIGINT; a lazy sequence of numbers starting @low+@gap and incrimenting by @gap. N2
      will always be greater than N1 by @gap. N2 can also be thought of as:
      LEAD(N1,1,N1+@gap) OVER (ORDER BY RN)

[Dependencies]:
N/A

[Developer Notes]:
 1. The lowest and highest possible numbers returned are whatever is allowable by a 
    bigint. The function, however, returns no more than 531,441,000,000 rows (8100^3). 
 2. @gap does not affect RN, RN will begin at @row1 and increase by 1 until the last row
    unless its used in a subquery where a filter is applied to RN.
 3. @gap must be greater than 0 or the function will not return any rows.
 4. Keep in mind that when @row1 is 0 then the highest RN value (ROWNUMBER) will be the 
    number of rows returned minus 1
 5. If you only need is a sequential set beginning at 0 or 1 then, for best performance
    use the RN column. Use N1 and/or N2 when you need to begin your sequence at any 
    number other than 0 or 1 or if you need a gap between your sequence of numbers. 
 6. Although @gap is a bigint it must be a positive integer or the function will
    not return any rows.
 7. The function will not return any rows when one of the following conditions are true:
      * any of the input parameters are NULL
      * @high is less than @low 
      * @gap is not greater than 0
    To force the function to return all NULLs instead of not returning anything you can
    add the following code to the end of the query:

      UNION ALL 
      SELECT NULL, NULL, NULL, NULL
      WHERE NOT (@high&@low&@gap&@row1 IS NOT NULL AND @high >= @low AND @gap > 0)

    This code was excluded as it adds a ~5% performance penalty.
 8. There is no performance penalty for sorting by rn ASC; there is a large performance 
    penalty for sorting in descending order WHEN @row1 = 1; WHEN @row1 = 0
    If you need a descending sort the use OP in place of RN then sort by rn ASC. 
 9. For 2012+ systems, The TOP logic can be replaced with:
   OFFSET 0 ROWS FETCH NEXT 
     ABS((ISNULL(@high,0)-ISNULL(@low,0))/ISNULL(@gap,0)+ISNULL(@row1,1)) ROWS ONLY

Best Practices:
--===== 1. Using RN (rownumber)
 -- (1.1) The best way to get the numbers 1,2,3...@high (e.g. 1 to 5):
 SELECT r.RN
 FROM   dbo.rangeAB(1,5,1,1) AS r;

 -- (1.2) The best way to get the numbers 0,1,2...@high-1 (e.g. 0 to 5):
 SELECT r.RN
 FROM   dbo.rangeAB(0,5,1,0) AS r;

--===== 2. Using OP for descending sorts without a performance penalty
 -- (2.1) The best way to get the numbers 5,4,3...@high (e.g. 5 to 1):
 SELECT   r.OP
 FROM     dbo.rangeAB(1,5,1,1) AS r 
 ORDER BY R.RN ASC;

 -- (2.2) The best way to get the numbers 0,1,2...@high-1 (e.g. 5 to 0):
 SELECT   r.OP 
 FROM     dbo.rangeAB(1,6,1,0) AS r
 ORDER BY r.RN ASC;

--===== 3. Using N1
 -- (3.1) To begin with numbers other than 0 or 1 use N1 (e.g. -3 to 3):
 SELECT r.N1
 FROM   dbo.rangeAB(-3,3,1,1) AS r;

 -- (3.2) ROW_NUMBER() is built in. If you want a ROW_NUMBER() include RN:
 SELECT r.RN, r.N1
 FROM   dbo.rangeAB(-3,3,1,1) AS r;

 -- (3.3) If you wanted a ROW_NUMBER() that started at 0 you would do this:
 SELECT r.RN, r.N1
 FROM dbo.rangeAB(-3,3,1,0) AS r;

--===== 4. Using N2 and @gap
 -- (4.1) To get 0,10,20,30...100, set @low to 0, @high to 100 and @gap to 10:
 SELECT r.N1
 FROM   dbo.rangeAB(0,100,10,1) AS r;
 -- (4.2) Note that N2=N1+@gap; this allows you to create a sequence of ranges.
 --       For example, to get (0,10),(10,20),(20,30).... (90,100):

 SELECT r.N1, r.N2
 FROM  dbo.rangeAB(0,90,10,1) AS r;

-----------------------------------------------------------------------------------------
[Revision History]:
 Rev 00 - 20140518 - Initial Development - AJB
 Rev 01 - 20151029 - Added 65 rows. Now L1=465; 465^3=100.5M. Updated comments - AJB
 Rev 02 - 20180613 - Complete re-design including opposite number column (op)
 Rev 03 - 20180920 - Added additional CROSS JOIN to L2 for 530B rows max - AJB
 Rev 04 - 20190306 - Added inline aliasing function(f): 
                     f.R=(@high-@low)/@gap, f.N=@gap+@low - AJB
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH
L1(N)  AS 
(
  SELECT 1
  FROM (VALUES
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
   (0),(0)) T(N) -- 90 values
),
L2(N)  AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c),
iTally AS (SELECT RN = ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b)
SELECT r.RN, r.OP, r.N1, r.N2
FROM
(
  SELECT
    RN = 0,
    OP = (@high-@low)/@gap,
    N1 = @low,
    N2 = @gap+@low
  WHERE @row1 = 0
  UNION ALL
  SELECT TOP (ABS((ISNULL(@high,0)-ISNULL(@low,0))/ISNULL(@gap,0)+ISNULL(@row1,1)))
    RN = i.RN,
    OP = (@high-@low)/@gap+(2*@row1)-i.RN,
    N1 = (i.rn-@row1)*@gap+@low,
    N2 = (i.rn-(@row1-1))*@gap+@low
  FROM       iTally AS i
  ORDER BY   i.RN
) AS r
WHERE @high&@low&@gap&@row1 IS NOT NULL AND @high >= @low 
AND   @gap > 0;
GO

CREATE FUNCTION samd.NGrams8k
(
  @string VARCHAR(8000), -- Input string
  @N      INT            -- requested token size
)
/*****************************************************************************************
[Purpose]:
 A character-level N-Grams function that outputs a contiguous stream of @N-sized tokens
 based on an input string (@string). Accepts strings up to 8000 varchar characters long.

[Author]: 
 Alan Burstein

[Compatibility]:
 SQL Server 2008+, Azure SQL Database

[Syntax]:
--===== Autonomous
 SELECT ng.position, ng.token 
 FROM   samd.NGrams8k(@string,@N) AS ng;

--===== Against a table using APPLY
 SELECT      s.SomeID, ng.position, ng.token
 FROM        dbo.SomeTable AS s
 CROSS APPLY samd.NGrams8K(s.SomeValue,@N) AS ng;

[Parameters]:
 @string  = The input string to split into tokens.
 @N       = The size of each token returned.

[Returns]:
 Position = BIGINT; the position of the token in the input string
 token    = VARCHAR(8000); a @N-sized character-level N-Gram token

[Dependencies]:
 1. dbo.rangeAB (iTVF)

[Revision History]:
------------------------------------------------------------------------------------------
 Rev 00 - 20140310 - Initial Development - Alan Burstein
 Rev 01 - 20150522 - Removed DQS N-Grams functionality, improved iTally logic. Also Added
                     conversion to bigint in the TOP logic to remove implicit conversion
                     to bigint - Alan Burstein
 Rev 03 - 20150909 - Added logic to only return values if @N is greater than 0 and less
                     than the length of @string. Updated comment section. - Alan Burstein
 Rev 04 - 20151029 - Added ISNULL logic to the TOP clause for the @string and @N
                     parameters to prevent a NULL string or NULL @N from causing "an
                     improper value" being passed to the TOP clause. - Alan Burstein
 Rev 05 - 20171228 - Small simplification; changed: 
                (ABS(CONVERT(BIGINT,(DATALENGTH(ISNULL(@string,''))-(ISNULL(@N,1)-1)),0)))
                                           to:
                (ABS(CONVERT(BIGINT,(DATALENGTH(ISNULL(@string,''))+1-ISNULL(@N,1)),0)))
 Rev 06 - 20180612 - Using CHECKSUM(N) in the to convert N in the token output instead of
                     using (CAST N as int). CHECKSUM removes the need to convert to int.
 Rev 07 - 20180612 - re-designed to: Use dbo.rangeAB - Alan Burstein
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT
  position   = r.RN,
  token      = SUBSTRING(@string, CHECKSUM(r.RN), @N)
FROM  dbo.rangeAB(1, LEN(@string)+1-@N,1,1) AS r
WHERE @N > 0 AND @N <= LEN(@string);
GO

CREATE FUNCTION samd.hammingDistance8K 
(
  @s1 VARCHAR(8000), -- first input string
  @s2 VARCHAR(8000)  -- second input string
)
/*****************************************************************************************
[Purpose]:
 Purely set-based iTVF that returns the Hamming Distance between two strings of equal 
 length. See: https://en.wikipedia.org/wiki/Hamming_distance

[Author]:
 Alan Burstein

[Compatibility]:
 SQL Server 2008+

[Syntax]:
--===== Autonomous
 SELECT h.HD
 FROM   samd.hammingDistance8K(@s1,@s2) AS h;

--===== Against a table using APPLY
 SELECT t.string, S2 = @s2, h.HD
 FROM   dbo.someTable AS t
 CROSS 
 APPLY  samd.hammingDistance8K(t.string, @s2) AS h;

[Parameters]:
  @s1 = VARCHAR(8000); the first input string
  @s2 = VARCHAR(8000); the second input string

[Dependencies]:
 1. samd.NGrams8K

[Examples]:
--===== 1. Basic Use
DECLARE @s1 VARCHAR(8000) = 'abc1234',
        @s2 VARCHAR(8000) = 'abc2234';

SELECT h.HD, h.L, h.minSim
FROM   samd.hammingDistance8K(@s1,@s2) AS h;

---------------------------------------------------------------------------------------
[Revision History]: 
 Rev 00 - 20180800 - Initial re-design - Alan Burstein
 Rev 01 - 20181116 - Added L (Length) and minSim
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT H.HD, H.L, minSim = 1.*(H.L-H.HD)/H.L
FROM
( 
  SELECT LEN(@s1)-SUM(CHARINDEX(ng.token,SUBSTRING(@S2,ng.position,1))), 
         CASE LEN(@s1) WHEN LEN(@s2) THEN LEN(@s1) END
  FROM   samd.NGrams8k(@s1,1) AS ng
  WHERE  LEN(@S1)=LEN(@S2)
) AS H(HD,L);
GO
...