Как я могу преобразовать первые 100 миллионов натуральных чисел в строки?

13

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

Я постараюсь начать с несколько формального определения. Строка включается в серию, если она состоит только из заглавных букв от 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, ...

Аналогия не совсем идеальна, потому что «А» ведет себя не так, как 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 с отброшенным набором результатов, а не сохранил его в таблице:

отменить набор результатов

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

Какие методы эффективной генерации набора данных описаны выше? Мартин Смит отметил, что хранимая процедура CLR, вероятно, не очень подходит из-за накладных расходов, связанных с обработкой такого количества строк.

Джо Оббиш
источник

Ответы:

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-символьными строками, полученными по мере необходимости. Отделение префикса от суффикса позволяет избежать сортировки.

План выполнения

100 миллионов строк


* В приведенном выше SQL нет ничего, что напрямую указывало бы на сохранение порядка . Оптимизатор выбирает физические операторы со свойствами, соответствующими спецификации SQL-запроса, включая порядок верхнего уровня по. Здесь он выбирает конкатенацию, реализованную физическим оператором объединения слиянием, чтобы избежать сортировки.

Гарантия состоит в том, что план выполнения обеспечивает семантический запрос и порядок верхнего уровня по спецификации. Знание, что concat merge объединяет порядок сохранения, позволяет составителю запросов предвидеть план выполнения, но оптимизатор выполнит его только в том случае, если ожидание является действительным.

Пол Уайт 9
источник
6

Я отправлю ответ, чтобы начать. Моя первая мысль была о том, что должна быть возможность воспользоваться сохраняющим порядок характером объединения вложенных циклов вместе с несколькими вспомогательными таблицами, которые имеют одну строку для каждой буквы. Сложная часть будет зацикливаться таким образом, чтобы результаты были упорядочены по длине, а также избегать дубликатов. Например, при перекрестном соединении CTE, который включает в себя все 26 заглавных букв вместе с '', вы можете в конечном итоге генерировать 'A' + '' + 'A'и, '' + 'A' + 'A'конечно же, одну и ту же строку.

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

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

По сравнению с использованием CTE запрос занимал в 3 раза больше времени для кластеризованной таблицы и в 4 раза больше для кучи. Я не верю, что проблема в том, что данные на диске. Он должен быть прочитан в память как одна страница и обработан в памяти для всего плана. Возможно, SQL Server может работать с данными оператора постоянного сканирования более эффективно, чем с данными, хранящимися на обычных страницах хранилища строк.

Интересно, что SQL Server предпочитает помещать упорядоченные результаты из одностраничной таблицы tempdb с упорядоченными данными в катушку таблицы:

плохой спул

SQL Server часто помещает результаты для внутренней таблицы перекрестного соединения в таблицу, даже если это кажется бессмысленным. Я думаю, что оптимизатору нужно немного поработать в этой области. Я запустил запрос, NO_PERFORMANCE_SPOOLчтобы избежать снижения производительности.

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

постоянный порядок сканирования

Однако лучше не рисковать, особенно если есть способ сделать это без значительного снижения производительности. Можно упорядочить данные в производной таблице, добавив лишний TOPоператор. Например:

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

Это дополнение к запросу должно гарантировать, что результаты будут возвращены в правильном порядке. Я ожидал, что все сорта окажут большое негативное влияние на производительность. Оптимизатор запросов также ожидал этого исходя из предполагаемых затрат:

дорогие сорта

Удивительно, но я не смог заметить какой-либо статистически значимой разницы во времени процессора или времени выполнения с явным упорядочением или без него. Во всяком случае, запрос казался быстрее с 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присоединяется к пяти разам, поскольку содержит строку для пустого символа. Последний символ в строке не должно быть пустым поэтому отдельное КТР определяется для него FIRST_CHAR. Столбец с дополнительным флагом ALL_CHARиспользуется для предотвращения дублирования, как описано выше. Может быть более эффективный способ сделать эту проверку, но есть определенно более неэффективные способы сделать это. Одна попытка была сделана мной LEN()и POWER()заставила запрос выполняться в шесть раз медленнее, чем текущая версия.

MAXDOP 1И FORCE ORDERподсказки необходимы , чтобы убедиться , что порядок сохраняется в запросе. Аннотированный примерный план может быть полезен, чтобы понять, почему объединения находятся в их текущем порядке:

аннотированный расчетный

Планы запросов часто читаются справа налево, но запросы строк выполняются слева направо. В идеале SQL Server будет запрашивать ровно 100 миллионов строк у d1оператора постоянного сканирования. При перемещении слева направо я ожидаю, что у каждого оператора будет запрашиваться меньше строк. Мы можем видеть это в фактическом плане выполнения . Кроме того, ниже приведен скриншот из SQL Sentry Plan Explorer:

исследователь

Мы получили ровно 100 миллионов строк из d1, и это хорошо. Обратите внимание, что соотношение строк между d2 и d3 почти точно составляет 27: 1 (165336 * 27 = 4464072), что имеет смысл, если подумать о том, как будет работать перекрестное соединение. Соотношение строк между d1 и d2 составляет 22,4, что представляет собой потраченную впустую работу. Я полагаю, что дополнительные строки взяты из дубликатов (из-за пустых символов в середине строк), которые не проходят через оператор соединения с вложенным циклом, который выполняет фильтрацию.

LOOP JOINНамек технически ненужный , потому что CROSS JOINможет быть реализована только в виде петли присоединиться к SQL Server. Это NO_PERFORMANCE_SPOOLпредотвращает ненужную буферизацию таблицы. Пропуск подсказки с катушкой заставил запрос на моей машине работать в 3 раза дольше.

Последний запрос имеет время процессора около 17 секунд и общее время, прошедшее до 18 секунд. Это было при выполнении запроса через SSMS и отбрасывании результирующего набора. Мне очень интересно увидеть другие методы генерации данных.

Джо Оббиш
источник
2

У меня есть решение, оптимизированное для получения строкового кода для любого конкретного числа до 217 180 147 158 (8 символов). Но я не могу побить ваше время

На моем компьютере с SQL Server 2014 ваш запрос занимает 18 секунд, а на моем - 3 м 46 с. Оба запроса используют недокументированный флаг трассировки 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 символов

Другой используемый трюк состоит в том, чтобы просто использовать сумму, чтобы получить правильное значение, а не пытаться выполнить конкататацию. Чтобы добиться этого, я просто смещаю цифры от основания 26 до основания 256 и добавляю значение ascii 'A' для каждой цифры. Таким образом, мы получаем бинарное представление искомой строки. После этого некоторые строковые манипуляции завершают процесс.

Adán Bucio
источник
-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
KumarHarsh
источник
Похоже, производная таблица преобразуется в единый вычислительный скаляр, который содержит более 400000 символов кода. Я подозреваю, что в этом расчете много накладных расходов. Вы можете попробовать что-то похожее на следующее: dbfiddle.uk/… Не стесняйтесь интегрировать компоненты этого в ваш ответ.
Джо Оббиш