如何将前1亿个正整数转换为字符串?


13

这与实际问题有些牵连。如果提供上下文帮助,则生成此数据可能对处理字符串的性能测试方式,生成需要在游标中对其应用某些操作的字符串或生成敏感数据的唯一匿名名称替换有用。我只是对在SQL Server中生成数据的有效方式感兴趣,请不要问为什么我需要生成此数据。

我将尝试从一个正式的定义开始。如果字符串仅由A-Z的大写字母组成,则包含在该系列中。该系列的第一项是“ A”。该系列由所有有效字符串组成,这些字符串按长度优先,然后按典型字母顺序排列。如果字符串在名为的列的表中STRING_COL,则该顺序可以在T-SQL中定义为ORDER BY LEN(STRING_COL) ASC, STRING_COL ASC

为了给出一个不太正式的定义,请查看excel中按字母顺序排列的列标题。该系列是相同的模式。考虑如何将整数转换为以26为底的数字:

1-> A,2-> B,3-> C,...,25-> Y,26-> Z,27-> AA,28-> AB,...

这个类比不是很完美,因为“ A”的行为不同于以10为底的0。下表列出了一些选定的值,希望可以使其更加清楚:

╔════════════╦════════╗
 ROW_NUMBER  STRING 
╠════════════╬════════╣
          1  A      
          2  B      
         25  Y      
         26  Z      
         27  AA     
         28  AB     
         51  AY     
         52  AZ     
         53  BA     
         54  BB     
      18278  ZZZ    
      18279  AAAA   
     475253  ZZZY   
     475254  ZZZZ   
     475255  AAAAA  
  100000000  HJUNYV 
╚════════════╩════════╝

目标是编写一个SELECT查询,以上面定义的顺序返回前100000000个字符串。我通过在SSMS中运行查询来进行测试,结果集被丢弃,而不是将其保存到表中:

丢弃结果集

理想情况下,查询将相当有效。在这里,我将效率定义为串行查询的cpu时间和并行查询的经过时间。您可以使用喜欢的任何未记录的技巧。也可以依靠未定义或无法保证的行为,但是如果您在答案中指出这一点,将不胜感激。

有哪些有效地生成上述数据集的方法?马丁·史密斯(Martin Smith)指出,由于处理这么多行的开销,CLR存储过程可能不是一个好方法。

Answers:


7

您的解决方案在笔记本电脑上运行35秒钟。以下代码耗时26秒(包括创建和填充临时表):

临时表

DROP TABLE IF EXISTS #T1, #T2, #T3, #T4;

CREATE TABLE #T1 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T2 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T3 (string varchar(6) NOT NULL PRIMARY KEY);
CREATE TABLE #T4 (string varchar(6) NOT NULL PRIMARY KEY);

INSERT #T1 (string)
VALUES
    ('A'), ('B'), ('C'), ('D'), ('E'), ('F'), ('G'),
    ('H'), ('I'), ('J'), ('K'), ('L'), ('M'), ('N'),
    ('O'), ('P'), ('Q'), ('R'), ('S'), ('T'), ('U'),
    ('V'), ('W'), ('X'), ('Y'), ('Z');

INSERT #T2 (string)
SELECT T1a.string + T1b.string
FROM #T1 AS T1a, #T1 AS T1b;

INSERT #T3 (string)
SELECT #T2.string + #T1.string
FROM #T2, #T1;

INSERT #T4 (string)
SELECT #T3.string + #T1.string
FROM #T3, #T1;

此处的想法是预先填充最多四个字符的有序组合。

主要代号

SELECT TOP (100000000)
    UA.string + UA.string2
FROM
(
    SELECT U.Size, U.string, string2 = '' FROM 
    (
        SELECT Size = 1, string FROM #T1
        UNION ALL
        SELECT Size = 2, string FROM #T2
        UNION ALL
        SELECT Size = 3, string FROM #T3
        UNION ALL
        SELECT Size = 4, string FROM #T4
    ) AS U
    UNION ALL
    SELECT Size = 5, #T1.string, string2 = #T4.string
    FROM #T1, #T4
    UNION ALL
    SELECT Size = 6, #T2.string, #T4.string
    FROM #T2, #T4
) AS UA
ORDER BY 
    UA.Size, 
    UA.string, 
    UA.string2
OPTION (NO_PERFORMANCE_SPOOL, MAXDOP 1);

这是四个预先计算的表的简单的保留顺序的联合*,并根据需要导出了5个字符和6个字符的字符串。将前缀与后缀分开可以避免排序。

执行计划

1亿行


*上面的SQL中没有什么可以直接指定保留订单的联合。优化器选择具有与SQL查询规范匹配的属性的物理运算符,包括顶级排序依据。在这里,它选择由合并联接物理运算符实现的串联以避免排序。

保证是执行计划按规范传递查询语义和顶级顺序。知道合并连接concat保留顺序可以使查询编写器预期执行计划,但是优化器仅在预期有效时才会交付。


6

我将发布答案以开始使用。我的第一个想法是,应该可以利用嵌套循环连接的顺序保留特性以及一些每个字母都有一行的辅助表。棘手的部分将以这样一种方式循环:结果按长度排序,并避免重复。例如,当交叉连接包含全部26个大写字母和''的CTE时,您最终可以生成'A' + '' + 'A''' + 'A' + 'A'并且当然是相同的字符串。

第一个决定是在哪里存储帮助程序数据。我尝试使用临时表,但是即使数据适合单个页面,这也会对性能产生令人惊讶的负面影响。临时表包含以下数据:

SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'

与使用CTE相比,使用群集表的查询花费了3倍的时间,而使用堆则花费了4倍的时间。我不认为问题在于数据在磁盘上。应该将其作为一个页面读入内存,并在整个计划中在内存中进行处理。也许SQL Server可以比使用常规行存储页面中存储的数据更有效地处理来自Constant Scan运算符的数据。

有趣的是,SQL Server选择将来自单页tempdb表的有序结果和有序数据放入表假脱机中:

坏口子

SQL Server通常将交叉联接的内部表的结果放入表假脱机中,即使这样做似乎很不明智。我认为优化器在这方面需要一些工作。我使用来运行查询NO_PERFORMANCE_SPOOL以避免性能下降。

使用CTE存储帮助程序数据的一个问题是不能保证对数据进行排序。我想不出为什么优化器会选择不对它进行排序,而在所有测试中,数据都是按照编写CTE的顺序进行处理的:

恒定扫描顺序

但是,最好不要冒险,尤其是如果有一种方法可以在不增加性能开销的情况下。通过添加多余的TOP运算符可以对派生表中的数据进行排序。例如:

(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)

该查询的补充应保证结果将以正确的顺序返回。我希望所有这些都会对性能产生很大的负面影响。查询优化器也基于估计的成本预期了这一点:

昂贵的种类

非常令人惊讶的是,无论是否进行显式排序,我都无法观察到CPU时间或运行时的任何统计学显着差异。如果有的话,使用ORDER BY!似乎查询运行得更快。我对此行为没有任何解释。

问题的棘手部分是弄清楚如何在正确的位置插入空白字符。如前所述,简单CROSS JOIN将导致重复数据。我们知道第100000000个字符串的长度为六个字符,因为:

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000

26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000

因此,我们只需要加入CTE六次。假设我们六次加入​​CTE,从每个CTE那里抓一封信,然后将它们全部串联在一起。假设最左边的字母不为空。如果随后的任何字母为空,则表示该字符串的长度少于六个字符,因此它是重复的字符串。因此,我们可以通过查找第一个非空白字符并要求之后的所有字符也不为空白来防止重复。我选择通过FLAG为一个CTE 分配一列并在该WHERE子句中添加一个检查来跟踪此情况。查看查询后,这应该会更清楚。最终查询如下:

WITH FIRST_CHAR (CHR) AS
(
    SELECT 'A'
    UNION ALL SELECT 'B'
    UNION ALL SELECT 'C'
    UNION ALL SELECT 'D'
    UNION ALL SELECT 'E'
    UNION ALL SELECT 'F'
    UNION ALL SELECT 'G'
    UNION ALL SELECT 'H'
    UNION ALL SELECT 'I'
    UNION ALL SELECT 'J'
    UNION ALL SELECT 'K'
    UNION ALL SELECT 'L'
    UNION ALL SELECT 'M'
    UNION ALL SELECT 'N'
    UNION ALL SELECT 'O'
    UNION ALL SELECT 'P'
    UNION ALL SELECT 'Q'
    UNION ALL SELECT 'R'
    UNION ALL SELECT 'S'
    UNION ALL SELECT 'T'
    UNION ALL SELECT 'U'
    UNION ALL SELECT 'V'
    UNION ALL SELECT 'W'
    UNION ALL SELECT 'X'
    UNION ALL SELECT 'Y'
    UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
    SELECT '', 0 CHR
    UNION ALL SELECT 'A', 1
    UNION ALL SELECT 'B', 1
    UNION ALL SELECT 'C', 1
    UNION ALL SELECT 'D', 1
    UNION ALL SELECT 'E', 1
    UNION ALL SELECT 'F', 1
    UNION ALL SELECT 'G', 1
    UNION ALL SELECT 'H', 1
    UNION ALL SELECT 'I', 1
    UNION ALL SELECT 'J', 1
    UNION ALL SELECT 'K', 1
    UNION ALL SELECT 'L', 1
    UNION ALL SELECT 'M', 1
    UNION ALL SELECT 'N', 1
    UNION ALL SELECT 'O', 1
    UNION ALL SELECT 'P', 1
    UNION ALL SELECT 'Q', 1
    UNION ALL SELECT 'R', 1
    UNION ALL SELECT 'S', 1
    UNION ALL SELECT 'T', 1
    UNION ALL SELECT 'U', 1
    UNION ALL SELECT 'V', 1
    UNION ALL SELECT 'W', 1
    UNION ALL SELECT 'X', 1
    UNION ALL SELECT 'Y', 1
    UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
    CASE 
    WHEN d6.FLAG = 1 THEN 5
    WHEN d5.FLAG = 1 THEN 4
    WHEN d4.FLAG = 1 THEN 3
    WHEN d3.FLAG = 1 THEN 2
    WHEN d2.FLAG = 1 THEN 1
    ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);

CTE如上所述。ALL_CHAR之所以加入五次,是因为其中包括一行空白字符。字符串中的最后一个字符绝不能为空,因此为其定义了单独的CTE FIRST_CHAR。如上所述,额外的标志列ALL_CHAR用于防止重复。可能有一种更有效的方法来执行此检查,但肯定有效率更低的方法。一个试图通过我LEN()POWER()作出的查询运行速度比目前的版本慢六倍。

MAXDOP 1FORCE ORDER提示是必要的,以确保订单查询保存。带注释的估计计划可能会有助于了解为什么联接按当前顺序排列:

带注释的估计

查询计划通常从右到左读取,但是行请求从左到右发生。理想情况下,SQL Server将向d1常量扫描运算符精确地请求1亿行。当您从左向右移动时,我希望每个运算符将请求更少的行。我们可以在实际执行计划中看到这一点。此外,下面是SQL Sentry Plan Explorer的屏幕截图:

探险家

我们从d1中获得了1亿行,这是一件好事。请注意,d2和d3之间的行比几乎完全是27:1(165336 * 27 = 4464072),这在考虑交叉连接的工作原理时很有意义。d1和d2之间的行比为22.4,这表示有些浪费的工作。我相信多余的行来自重复项(由于字符串中间有空白字符),因此它们不会超过执行过滤的嵌套循环联接运算符。

LOOP JOIN提示在技术上是不必要的,因为a CROSS JOIN只能在SQL Server中实现为循环联接。这NO_PERFORMANCE_SPOOL是为了防止不必要的表假脱机。省略后台打印提示使查询在我的计算机上花费的时间延长了3倍。

最终查询的cpu时间约为17秒,总经过时间为18秒。那是通过SSMS运行查询并丢弃结果集时的情况。我对查看其他生成数据的方法非常感兴趣。


2

我有一个优化的解决方案,可以获取任何特定数字的字符串代码,最多217,180,147,158(8个字符)。但是我不能打败你的时间:

在我的机器上,使用SQL Server 2014,您的查询需要18秒,而我的需要3m 46s。这两个查询都使用未记录的跟踪标志8690,因为2014不支持该NO_PERFORMANCE_SPOOL提示。

这是代码:

/* precompute offsets and powers to simplify final query */
CREATE TABLE #ExponentsLookup (
    offset          BIGINT NOT NULL,
    offset_end      BIGINT NOT NULL,
    position        INTEGER NOT NULL,
    divisor         BIGINT NOT NULL,
    shifts          BIGINT NOT NULL,
    chars           INTEGER NOT NULL,
    PRIMARY KEY(offset, offset_end, position)
);

WITH base_26_multiples AS ( 
    SELECT  number  AS exponent,
            CAST(POWER(26.0, number) AS BIGINT) AS multiple
    FROM    master.dbo.spt_values
    WHERE   [type] = 'P'
            AND number < 8
),
num_offsets AS (
    SELECT  *,
            -- The maximum posible value is 217180147159 - 1
            LEAD(offset, 1, 217180147159) OVER(
                ORDER BY exponent
            ) AS offset_end
    FROM    (
                SELECT  exponent,
                        SUM(multiple) OVER(
                            ORDER BY exponent
                        ) AS offset
                FROM    base_26_multiples
            ) x
)
INSERT INTO #ExponentsLookup(offset, offset_end, position, divisor, shifts, chars)
SELECT  ofst.offset, ofst.offset_end,
        dgt.number AS position,
        CAST(POWER(26.0, dgt.number) AS BIGINT)     AS divisor,
        CAST(POWER(256.0, dgt.number) AS BIGINT)    AS shifts,
        ofst.exponent + 1                           AS chars
FROM    num_offsets ofst
        LEFT JOIN master.dbo.spt_values dgt --> as many rows as resulting chars in string
            ON [type] = 'P'
            AND dgt.number <= ofst.exponent;

/*  Test the cases in table example */
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    (
            VALUES(1),(2),(25),(26),(27),(28),(51),(52),(53),(54),
            (18278),(18279),(475253),(475254),(475255),
            (100000000), (CAST(217180147158 AS BIGINT))
        ) ref([row_number])
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number];

/*  Test with huge set  */
WITH numbers AS (
    SELECT  TOP(100000000)
            ROW_NUMBER() OVER(
                ORDER BY x1.number
            ) AS [row_number]
    FROM    master.dbo.spt_values x1
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 676) x3
    WHERE   x1.number < 219
)
SELECT  /*  1.- Get the base 26 digit and then shift it to align it to 8 bit boundaries
            2.- Sum the resulting values
            3.- Bias the value with a reference that represent the string 'AAAAAAAA'
            4.- Take the required chars */
        ref.[row_number],
        REVERSE(SUBSTRING(REVERSE(CAST(SUM((((ref.[row_number] - ofst.offset) / ofst.divisor) % 26) * ofst.shifts) +
            CAST(CAST('AAAAAAAA' AS BINARY(8)) AS BIGINT) AS BINARY(8))),
            1, MAX(ofst.chars))) AS string
FROM    numbers ref
        LEFT JOIN #ExponentsLookup ofst
            ON ofst.offset <= ref.[row_number]
            AND ofst.offset_end > ref.[row_number]
GROUP BY
        ref.[row_number]
ORDER BY
        ref.[row_number]
OPTION (QUERYTRACEON 8690);

这里的技巧是预先计算不同排列的开始位置:

  1. 当必须输出单个字符时,您将有26 ^ 1个从26 ^ 0开始的排列。
  2. 当您必须输出2个字符时,您有26 ^ 2个排列,从26 ^ 0 + 26 ^ 1开始
  3. 当您必须输出3个字符时,您有26 ^ 3个排列,从26 ^ 0 + 26 ^ 1 + 26 ^ 2开始
  4. 重复n个字符

使用的另一个技巧是简单地使用sum来获得正确的值,而不是尝试连接。为了实现这一点,我只需将数字从26偏移到256,然后为每个数字添加'A'的ascii值。因此,我们获得了要查找的字符串的二进制表示形式。之后,一些字符串操作完成了该过程。


-1

好的,这是我最新的脚本。

没有循环,没有递归。

仅适用于6个字符

最大的缺点是大约需要22分钟才能完成1,00,00,000

这次我的剧本很短。

SET NoCount on

declare @z int=26
declare @start int=@z+1 
declare @MaxLimit int=10000000

SELECT TOP (@MaxLimit) IDENTITY(int,1,1) AS N
    INTO NumbersTest1
    FROM     master.dbo.spt_values x1   
   CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x2
            CROSS JOIN (SELECT number FROM master.dbo.spt_values WHERE [type] = 'P' AND number < 500) x3
    WHERE   x1.number < 219
ALTER TABLE NumbersTest1 ADD CONSTRAINT PK_NumbersTest1 PRIMARY KEY CLUSTERED (N)


select N, strCol from NumbersTest1
cross apply
(
select 
case when IntCol6>0 then  char((IntCol6%@z)+64) else '' end 
+case when IntCol5=0 then 'Z' else isnull(char(IntCol5+64),'') end 
+case when IntCol4=0 then 'Z' else isnull(char(IntCol4+64),'') end 
+case when IntCol3=0 then 'Z' else isnull(char(IntCol3+64),'') end 
+case when IntCol2=0 then 'Z' else isnull(char(IntCol2+64),'') end 
+case when IntCol1=0 then 'Z' else isnull(char(IntCol1+64),'') end strCol
from
(
select  IntCol1,IntCol2,IntCol3,IntCol4
,case when IntCol5>0 then  IntCol5%@z else null end IntCol5

,case when IntCol5/@z>0 and  IntCol5%@z=0 then  IntCol5/@z-1 
when IntCol5/@z>0 then IntCol5/@z
else null end IntCol6
from
(
select IntCol1,IntCol2,IntCol3
,case when IntCol4>0 then  IntCol4%@z else null end IntCol4

,case when IntCol4/@z>0 and  IntCol4%@z=0 then  IntCol4/@z-1 
when IntCol4/@z>0 then IntCol4/@z
else null end IntCol5
from
(
select IntCol1,IntCol2
,case when IntCol3>0 then  IntCol3%@z else null end IntCol3
,case when IntCol3/@z>0 and  IntCol3%@z=0 then  IntCol3/@z-1 
when IntCol3/@z>0 then IntCol3/@z
else null end IntCol4

from
(
select IntCol1
,case when IntCol2>0 then  IntCol2%@z else null end IntCol2
,case when IntCol2/@z>0 and  IntCol2%@z=0 then  IntCol2/@z-1 
when IntCol2/@z>0 then IntCol2/@z
else null end IntCol3

from
(
select case when N>0 then N%@z else null end IntCol1
,case when N%@z=0 and  (N/@z)>1 then (N/@z)-1 else  (N/@z) end IntCol2 

)Lv2
)Lv3
)Lv4
)Lv5
)LV6

)ca

DROP TABLE NumbersTest1

看起来派生表已转换为单个计算标量,该标量超过40万个字符。我怀疑该计算会产生很多开销。您可能想要尝试类似于以下内容的东西:dbfiddle.uk/… 可以将其组件集成到您的答案中。
Joe Obbish
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.