Каков наилучший способ создания и заполнения таблицы чисел?

Я видел много разных способов создания и заполнения таблицы чисел. Однако, каков наилучший способ создать и наполнить его? С "лучшим" определяется от самого к наименее важному:

  • Таблица создана с оптимальной индексацией
  • строки генерируются быстрее
  • простой код, используемый для создания и заполнения

Если вы не знаете, что такое таблица чисел, посмотрите здесь: Почему я должен рассмотреть возможность использования вспомогательной таблицы чисел?

12 ответов

Решение

Вот несколько примеров кода, взятых из Интернета и ответов на этот вопрос.

Для каждого метода я изменил исходный код, чтобы каждый из них использовал одну и ту же таблицу и столбец: NumbersTest и Number с 10 000 строк или как можно ближе к этому. Также я предоставил ссылки на место происхождения.

МЕТОД 1 здесь очень медленный метод зацикливания отсюда
в среднем 13,01 секунды
побежал 3 раза убрал самый высокий, вот времена в секундах: 12.42, 13.60

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
CREATE TABLE NumbersTest(Number INT IDENTITY(1,1)) 
SET NOCOUNT ON
WHILE COALESCE(SCOPE_IDENTITY(), 0) < 100000
BEGIN 
    INSERT dbo.NumbersTest DEFAULT VALUES 
END
SET NOCOUNT OFF
-- Add a primary key/clustered index to the numbers table
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE())/1000.0)+' seconds'
SELECT COUNT(*) FROM NumbersTest

МЕТОД 2 здесь гораздо быстрее, зацикливание отсюда
в среднем 1,1658 секунды
побежал 11 раз убрал самый высокий, вот времена в секундах: 1.117, 1.140, 1.203, 1.170, 1.173, 1.156, 1.203, 1.153, 1.173, 1.170

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
CREATE TABLE NumbersTest (Number INT NOT NULL);
DECLARE @i INT;
SELECT @i = 1;
SET NOCOUNT ON
WHILE @i <= 10000
BEGIN
    INSERT INTO dbo.NumbersTest(Number) VALUES (@i);
    SELECT @i = @i + 1;
END;
SET NOCOUNT OFF
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE())/1000.0)+' seconds'
SELECT COUNT(*) FROM NumbersTest

МЕТОД 3 Вот одна вставка на основе кода здесь
в среднем 488,6 миллисекунд
побежал 11 раз убрал самый высокий, вот времена в миллисекундах: 686, 673, 623, 686 343 343 376 360 343 453

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
CREATE TABLE NumbersTest (Number  int  not null)  
;WITH Nums(Number) AS
(SELECT 1 AS Number
 UNION ALL
 SELECT Number+1 FROM Nums where Number<10000
)
insert into NumbersTest(Number)
    select Number from Nums option(maxrecursion 10000)
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'
SELECT COUNT(*) FROM NumbersTest

МЕТОД 4 здесь - это метод "полупериода", который здесь составляет в среднем 348,3 миллисекунды (было трудно получить хорошее время из-за "GO" в середине кода, любые предложения будут оценены)
побежал 11 раз убрал самое высокое, вот времена в миллисекундах: 356, 360, 283, 346, 360, 376, 326, 373, 330, 373

DROP TABLE NumbersTest
DROP TABLE #RunDate
CREATE TABLE #RunDate (RunDate datetime)
INSERT INTO #RunDate VALUES(GETDATE())
CREATE TABLE NumbersTest (Number int NOT NULL);
INSERT NumbersTest values (1);
GO --required
INSERT NumbersTest SELECT Number + (SELECT COUNT(*) FROM NumbersTest) FROM NumbersTest
GO 14 --will create 16384 total rows
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
SELECT CONVERT(varchar(20),datediff(ms,RunDate,GETDATE()))+' milliseconds' FROM #RunDate
SELECT COUNT(*) FROM NumbersTest

МЕТОД 5, вот один ВСТАВКА из ответа Филиппа Келли
в среднем 92,7 миллисекунды
побежал в 11 раз убрал самое высокое, вот времена в миллисекундах: 80, 96, 96, 93, 110, 110, 80, 76, 93, 93

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
CREATE TABLE NumbersTest (Number  int  not null)  
;WITH
  Pass0 as (select 1 as C union all select 1), --2 rows
  Pass1 as (select 1 as C from Pass0 as A, Pass0 as B),--4 rows
  Pass2 as (select 1 as C from Pass1 as A, Pass1 as B),--16 rows
  Pass3 as (select 1 as C from Pass2 as A, Pass2 as B),--256 rows
  Pass4 as (select 1 as C from Pass3 as A, Pass3 as B),--65536 rows
  --I removed Pass5, since I'm only populating the Numbers table to 10,000
  Tally as (select row_number() over(order by C) as Number from Pass4)
INSERT NumbersTest
        (Number)
    SELECT Number
        FROM Tally
        WHERE Number <= 10000
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'
SELECT COUNT(*) FROM NumbersTest

МЕТОД 6, вот один ВСТАВКА от Mladen Prajdic ответ
в среднем 82,3 миллисекунды
побежал 11 раз убрал самое высокое, вот времена в миллисекундах: 80, 80, 93, 76, 93, 63, 93, 76, 93, 76

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
CREATE TABLE NumbersTest (Number  int  not null)  
INSERT INTO NumbersTest(Number)
SELECT TOP 10000 row_number() over(order by t1.number) as N
FROM master..spt_values t1 
    CROSS JOIN master..spt_values t2
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number);
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'
SELECT COUNT(*) FROM NumbersTest

МЕТОД 7 здесь это одна вставка на основе кода здесь
в среднем 56,3 миллисекунды
побежал 11 раз убрал самое высокое, вот времена в миллисекундах: 63, 50, 63, 46, 60, 63, 63, 46, 63, 46

DROP TABLE NumbersTest
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO NumbersTest
    FROM sys.objects s1       --use sys.columns if you don't get enough rows returned to generate all the numbers you need
    CROSS JOIN sys.objects s2 --use sys.columns if you don't get enough rows returned to generate all the numbers you need
ALTER TABLE NumbersTest ADD CONSTRAINT PK_NumbersTest PRIMARY KEY CLUSTERED (Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'
SELECT COUNT(*) FROM NumbersTest

Посмотрев на все эти методы, мне очень понравился метод 7, который был самым быстрым, а код довольно простым.

Я использую это, что быстро, как ад:

insert into Numbers(N)
select top 1000000 row_number() over(order by t1.number) as N
from   master..spt_values t1 
       cross join master..spt_values t2

Если вы просто делаете это в SQL Server Management Studio или sqlcmd, вы можете использовать тот факт, что разделитель пакетов позволяет повторить пакет:

CREATE TABLE Number (N INT IDENTITY(1,1) PRIMARY KEY NOT NULL);
GO

INSERT INTO Number DEFAULT VALUES;
GO 100000

Это вставит 100000 записей в Numbers Таблица.

Это медленно. Это сравнивается с МЕТОДОМ 1 в ответе @KM., Который является самым медленным из примеров. Тем не менее, он такой же легкий, как он есть. Вы могли бы немного ускорить его, добавив ограничение первичного ключа после пакета вставки.

Я начну со следующего шаблона, который получен из многочисленных печатных изданий рутины Ицик Бен-Гана:

;WITH
  Pass0 as (select 1 as C union all select 1), --2 rows
  Pass1 as (select 1 as C from Pass0 as A, Pass0 as B),--4 rows
  Pass2 as (select 1 as C from Pass1 as A, Pass1 as B),--16 rows
  Pass3 as (select 1 as C from Pass2 as A, Pass2 as B),--256 rows
  Pass4 as (select 1 as C from Pass3 as A, Pass3 as B),--65536 rows
  Pass5 as (select 1 as C from Pass4 as A, Pass4 as B),--4,294,967,296 rows
  Tally as (select row_number() over(order by C) as Number from Pass5)
 select Number from Tally where Number <= 1000000

Предложение "WHERE N<= 1000000" ограничивает выходную мощность от 1 до 1 миллиона и может быть легко настроено на желаемый диапазон.

Поскольку это предложение WITH, его можно преобразовать во INSERT... SELECT... примерно так:

--  Sample use: create one million rows
CREATE TABLE dbo.Example (ExampleId  int  not null)  

DECLARE @RowsToCreate int
SET @RowsToCreate = 1000000

--  "Table of numbers" data generator, as per Itzik Ben-Gan (from multiple sources)
;WITH
  Pass0 as (select 1 as C union all select 1), --2 rows
  Pass1 as (select 1 as C from Pass0 as A, Pass0 as B),--4 rows
  Pass2 as (select 1 as C from Pass1 as A, Pass1 as B),--16 rows
  Pass3 as (select 1 as C from Pass2 as A, Pass2 as B),--256 rows
  Pass4 as (select 1 as C from Pass3 as A, Pass3 as B),--65536 rows
  Pass5 as (select 1 as C from Pass4 as A, Pass4 as B),--4,294,967,296 rows
  Tally as (select row_number() over(order by C) as Number from Pass5)
INSERT Example (ExampleId)
 select Number
  from Tally
  where Number <= @RowsToCreate

Индексирование таблицы после ее создания будет самым быстрым способом ее индексирования.

О, и я бы назвал это таблицей "Tally". Я думаю, что это общий термин, и вы можете найти множество трюков и примеров, прибегая к помощи Google.

Вот короткое и быстрое решение в памяти, которое я придумал, используя конструкторы с табличными значениями, представленные в SQL Server 2008:

--1,000,000 rows.  Either add/remove CROSS JOINs, or use TOP clause to modify this

;WITH v AS (SELECT * FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) v(z))

SELECT N FROM (SELECT ROW_NUMBER() OVER (ORDER BY v1.z)-1 N FROM v v1 
    CROSS JOIN v v2 CROSS JOIN v v3 CROSS JOIN v v4 CROSS JOIN v v5 CROSS JOIN v v6) Nums

Обратите внимание, что это можно быстро рассчитать на лету или (что еще лучше) сохранить в постоянной таблице (просто добавьте INTO пункт после SELECT N сегмент) с первичным ключом на N поле для повышения эффективности.

Для тех, кто ищет решение Azure

SET NOCOUNT ON    
CREATE TABLE Numbers (n bigint PRIMARY KEY)    
GO    
DECLARE @numbers table(number int);  
WITH numbers(number) as  (   
SELECT 1 AS number   
UNION all   
SELECT number+1 FROM numbers WHERE number<10000  
)  
INSERT INTO @numbers(number)  
SELECT number FROM numbers OPTION(maxrecursion 10000)
INSERT INTO Numbers(n)  SELECT number FROM @numbers

Получено из блога команды sql azure http://azure.microsoft.com/blog/2010/09/16/create-a-numbers-table-in-sql-azure/

Я знаю, что эта ветка старая и на нее ответили, но есть способ выжать немного больше производительности из метода 7:

Вместо этого (по существу, метод 7, но с некоторой простотой использования, полировка):

DECLARE @BIT AS BIT = 0
IF OBJECT_ID('tempdb..#TALLY') IS NOT NULL
  DROP TABLE #TALLY
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO #TALLY
    FROM sys.objects s1       --use sys.columns if you don't get enough rows returned to generate all the numbers you need
    CROSS JOIN sys.objects s2 --use sys.co
ALTER TABLE #TALLY ADD PRIMARY KEY(Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'

Попробуй это:

DECLARE @BIT AS BIT = 0
IF OBJECT_ID('tempdb..#TALLY') IS NOT NULL
  DROP TABLE #TALLY
DECLARE @RunDate datetime
SET @RunDate=GETDATE()
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO #TALLY
    FROM        (SELECT @BIT [X] UNION ALL SELECT @BIT) [T2]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T4]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T8]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T16]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T32]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T64]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T128]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T256]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T512]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T1024]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T2048]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T4096]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T8192]
    CROSS JOIN  (SELECT @BIT [X] UNION ALL SELECT @BIT) [T16384]
ALTER TABLE #TALLY ADD PRIMARY KEY(Number)
PRINT CONVERT(varchar(20),datediff(ms,@RunDate,GETDATE()))+' milliseconds'

На моем сервере это занимает ~10 мс, а не ~16-20 мс при выборе из sys.objects. Это также имеет дополнительное преимущество, заключающееся в том, что не зависит от количества объектов в sys.objects. Хотя это довольно безопасно, технически это зависимость, а другая все равно идет быстрее. Я думаю, что повышение скорости сводится к использованию битов, если вы измените:

DECLARE @BIT AS BIT = 0

чтобы:

DECLARE @BIT AS BIGINT = 0

Это добавляет ~8-10 мс к общему времени на моем сервере. Тем не менее, когда вы масштабируете до 1 000 000 записей, BIT против BIGINT больше не оказывает существенного влияния на мой запрос, но он все равно работает на ~680 мс против ~730 мс от sys.objects.

Вот пара дополнительных методов:
Способ 1

IF OBJECT_ID('dbo.Numbers', 'U') IS NOT NULL
    DROP TABLE dbo.Numbers
GO

CREATE TABLE Numbers (Number int NOT NULL PRIMARY KEY);
GO

DECLARE @i int = 1;
INSERT INTO dbo.Numbers (Number) 
VALUES (1),(2);

WHILE 2*@i < 1048576
BEGIN
    INSERT INTO dbo.Numbers (Number) 
    SELECT Number + 2*@i
    FROM dbo.Numbers;
    SET @i = @@ROWCOUNT;
END
GO

SELECT COUNT(*) FROM Numbers AS RowCownt --1048576 rows

Способ 2

IF OBJECT_ID('dbo.Numbers', 'U') IS NOT NULL
    DROP TABLE dbo.Numbers
GO

CREATE TABLE dbo.Numbers (Number int NOT NULL PRIMARY KEY);
GO

DECLARE @i INT = 0; 
INSERT INTO dbo.Numbers (Number) 
VALUES (1);

WHILE @i <= 9
BEGIN
    INSERT INTO dbo.Numbers (Number)
    SELECT N.Number + POWER(4, @i) * D.Digit 
    FROM dbo.Numbers AS N
        CROSS JOIN (VALUES(1),(2),(3)) AS D(Digit)
    ORDER BY D.Digit, N.Number
    SET @i = @i + 1;
END
GO

SELECT COUNT(*) FROM dbo.Numbers AS RowCownt --1048576 rows

Способ 3

IF OBJECT_ID('dbo.Numbers', 'U') IS NOT NULL
    DROP TABLE dbo.Numbers
GO

CREATE TABLE Numbers (Number int identity NOT NULL PRIMARY KEY, T bit NULL);

WITH
    T1(T) AS (SELECT T FROM (VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS T(T)) --10 rows
   ,T2(T) AS (SELECT A.T FROM T1 AS A CROSS JOIN T1 AS B CROSS JOIN T1 AS C) --1,000 rows
   ,T3(T) AS (SELECT A.T FROM T2 AS A CROSS JOIN T2 AS B CROSS JOIN T2 AS C) --1,000,000,000 rows

INSERT INTO dbo.Numbers(T)
SELECT TOP (1048576) NULL
FROM T3;

ALTER TABLE Numbers
    DROP COLUMN T; 
GO

SELECT COUNT(*) FROM dbo.Numbers AS RowCownt --1048576 rows

Метод 4, взятый из книги Алексея Кузнецова " Оборонительное программирование баз данных "

IF OBJECT_ID('dbo.Numbers', 'U') IS NOT NULL
    DROP TABLE dbo.Numbers
GO

CREATE TABLE Numbers (Number int NOT NULL PRIMARY KEY);
GO

DECLARE @i INT = 1 ; 
INSERT INTO dbo.Numbers (Number) 
VALUES (1);

WHILE @i < 524289 --1048576
BEGIN; 
    INSERT INTO dbo.Numbers (Number) 
    SELECT Number + @i 
    FROM dbo.Numbers; 
    SET @i = @i * 2 ; 
END
GO

SELECT COUNT(*) FROM dbo.Numbers AS RowCownt --1048576 rows

Метод 5, взятый из статьи Erland Sommarskog из массивов и списков в SQL Server 2005 и далее.

IF OBJECT_ID('dbo.Numbers', 'U') IS NOT NULL
    DROP TABLE dbo.Numbers
GO

CREATE TABLE Numbers (Number int NOT NULL PRIMARY KEY);
GO

WITH digits (d) AS (
   SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL
   SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL
   SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL
   SELECT 0)
INSERT INTO Numbers (Number)
   SELECT Number
   FROM   (SELECT i.d + ii.d * 10 + iii.d * 100 + iv.d * 1000 +
                  v.d * 10000 + vi.d * 100000 AS Number
           FROM   digits i
           CROSS  JOIN digits ii
           CROSS  JOIN digits iii
           CROSS  JOIN digits iv
           CROSS  JOIN digits v
           CROSS  JOIN digits vi) AS Numbers
   WHERE  Number > 0
GO

SELECT COUNT(*) FROM dbo.Numbers AS RowCownt --999999 rows

Резюме:
Из этих 5 методов метод 3 представляется наиболее быстрым.

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

Я делаю то же самое с датами, имея таблицу, охватывающую от 10 лет в прошлом до 10 лет в будущем (и часы дня для более детальной отчетности). Это удобный способ получить значения для всех дат, даже если в ваших "реальных" таблицах данных нет данных для них.

У меня есть скрипт, который я использую для их создания, что-то вроде (это из памяти):

drop table numbers; commit;
create table numbers (n integer primary key); commit;
insert into numbers values (0); commit;
insert into numbers select n+1 from numbers; commit;
insert into numbers select n+2 from numbers; commit;
insert into numbers select n+4 from numbers; commit;
insert into numbers select n+8 from numbers; commit;
insert into numbers select n+16 from numbers; commit;
insert into numbers select n+32 from numbers; commit;
insert into numbers select n+64 from numbers; commit;

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

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

Некоторые из предложенных методов основаны на системных объектах (например, 'sys.objects'). Они предполагают, что эти системные объекты содержат достаточно записей для генерации наших чисел.

Я не буду опираться на то, что не относится к моему заявлению и над которым у меня нет полного контроля. Например: содержимое этих таблиц sys может измениться, таблицы могут перестать быть действительными в новой версии SQL и т. Д.

В качестве решения мы можем создать нашу собственную таблицу с записями. Затем мы используем этот объект вместо этих связанных с системой объектов (таблица со всеми числами должна быть в порядке, если мы заранее знаем диапазон, в противном случае мы могли бы выбрать тот, для которого выполняется перекрестное соединение).

Решение на основе CTE работает нормально, но имеет ограничения, связанные с вложенными циклами.

Это сделает это. Преимущества этого подхода:

  1. Больше контроля над нижними и верхними значениями. Если в какой-то момент вам пришлось отменить текущий запрос, вы можете изменить нижний диапазон, чтобы запустить процесс снова.
  2. Нет ограничений первичного ключа или удостоверения, которые могут повлиять на время выполнения запроса.
CREATE TABLE Numbers(N INT);

--

DECLARE @lower_range INT= 1;
DECLARE @upper_range INT= 10000;

--

WHILE(@lower_range <= @upper_range)
    BEGIN
        INSERT INTO Numbers(N)
    VALUES(@lower_range);
        SET @lower_range = @lower_range + 1;
    END;

--

SELECT *
FROM Numbers
ORDER BY N;

Это переупаковка принятого ответа - но таким образом, что вы можете сравнить их все друг с другом для себя - сравниваются 3 лучших алгоритма (и комментарии объясняют, почему другие методы исключены), и вы можете работать в соответствии с вашими настройками Посмотрите, как каждый из них работает с размером последовательности, которую вы хотите.

SET NOCOUNT ON;

--
-- Set the count of numbers that you want in your sequence ...
--
DECLARE @NumberOfNumbers int = 10000000;
--
--  Some notes on choosing a useful length for your sequence ...
--      For a sequence of  100 numbers -- winner depends on preference of min/max/avg runtime ... (I prefer PhilKelley algo here - edit the algo so RowSet2 is max RowSet CTE)
--      For a sequence of   1k numbers -- winner depends on preference of min/max/avg runtime ... (Sadly PhilKelley algo is generally lowest ranked in this bucket, but could be tweaked to perform better)
--      For a sequence of  10k numbers -- a clear winner emerges for this bucket
--      For a sequence of 100k numbers -- do not test any looping methods at this size or above ...
--                                        the previous winner fails, a different method is need to guarantee the full sequence desired
--      For a sequence of  1MM numbers -- the statistics aren't changing much between the algorithms - choose one based on your own goals or tweaks
--      For a sequence of 10MM numbers -- only one of the methods yields the desired sequence, and the numbers are much closer than for smaller sequences

DECLARE @TestIteration int = 0;
DECLARE @MaxIterations int = 10;
DECLARE @MethodName varchar(128);

-- SQL SERVER 2017 Syntax/Support needed
DROP TABLE IF EXISTS #TimingTest
CREATE TABLE #TimingTest (MethodName varchar(128), TestIteration int, StartDate DateTime2, EndDate DateTime2, ElapsedTime decimal(38,0), ItemCount decimal(38,0), MaxNumber decimal(38,0), MinNumber decimal(38,0))

--
--  Conduct the test ...
--
WHILE @TestIteration < @MaxIterations
BEGIN
    -- Be sure that the test moves forward
    SET @TestIteration += 1;

/*  -- This method has been removed, as it is BY FAR, the slowest method
    -- This test shows that, looping should be avoided, likely at all costs, if one places a value / premium on speed of execution ...

    --
    -- METHOD - Fast looping
    --

    -- Prep for the test
    DROP TABLE IF EXISTS [Numbers].[Test];
    CREATE TABLE [Numbers].[Test] (Number INT NOT NULL);

    -- Method information
    SET @MethodName = 'FastLoop';

    -- Record the start of the test
    INSERT INTO #TimingTest(MethodName, TestIteration, StartDate)
    SELECT @MethodName, @TestIteration, GETDATE()

    -- Run the algorithm
    DECLARE @i INT = 1;
    WHILE @i <= @NumberOfNumbers
    BEGIN
        INSERT INTO [Numbers].[Test](Number) VALUES (@i);
        SELECT @i = @i + 1;
    END;

    ALTER TABLE [Numbers].[Test] ADD CONSTRAINT PK_Numbers_Test_Number PRIMARY KEY CLUSTERED (Number)

    -- Record the end of the test
    UPDATE tt
        SET 
            EndDate = GETDATE()
    FROM #TimingTest tt
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    -- And the stats about the numbers in the sequence
    UPDATE tt
        SET 
            ItemCount = results.ItemCount,
            MaxNumber = results.MaxNumber,
            MinNumber = results.MinNumber
    FROM #TimingTest tt
    CROSS JOIN (
        SELECT COUNT(Number) as ItemCount, MAX(Number) as MaxNumber, MIN(Number) as MinNumber FROM [Numbers].[Test]
    ) results
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration
*/

/*  -- This method requires GO statements, which would break the script, also - this answer does not appear to be the fastest *AND* seems to perform "magic"
    --
    -- METHOD - "Semi-Looping"
    --

    -- Prep for the test
    DROP TABLE IF EXISTS [Numbers].[Test];
    CREATE TABLE [Numbers].[Test] (Number INT NOT NULL);

    -- Method information
    SET @MethodName = 'SemiLoop';

    -- Record the start of the test
    INSERT INTO #TimingTest(MethodName, TestIteration, StartDate)
    SELECT @MethodName, @TestIteration, GETDATE()

    -- Run the algorithm 
    INSERT [Numbers].[Test] values (1);
--    GO --required

    INSERT [Numbers].[Test] SELECT Number + (SELECT COUNT(*) FROM [Numbers].[Test]) FROM [Numbers].[Test]
--    GO 14 --will create 16384 total rows

    ALTER TABLE [Numbers].[Test] ADD CONSTRAINT PK_Numbers_Test_Number PRIMARY KEY CLUSTERED (Number)

    -- Record the end of the test
    UPDATE tt
        SET 
            EndDate = GETDATE()
    FROM #TimingTest tt
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    -- And the stats about the numbers in the sequence
    UPDATE tt
        SET 
            ItemCount = results.ItemCount,
            MaxNumber = results.MaxNumber,
            MinNumber = results.MinNumber
    FROM #TimingTest tt
    CROSS JOIN (
        SELECT COUNT(Number) as ItemCount, MAX(Number) as MaxNumber, MIN(Number) as MinNumber FROM [Numbers].[Test]
    ) results
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration
*/
    --
    -- METHOD - Philip Kelley's algo 
    --          (needs tweaking to match the desired length of sequence in order to optimize its performance, relies more on the coder to properly tweak the algorithm)
    --

    -- Prep for the test
    DROP TABLE IF EXISTS [Numbers].[Test];
    CREATE TABLE [Numbers].[Test] (Number INT NOT NULL);

    -- Method information
    SET @MethodName = 'PhilKelley';

    -- Record the start of the test
    INSERT INTO #TimingTest(MethodName, TestIteration, StartDate)
    SELECT @MethodName, @TestIteration, GETDATE()

    -- Run the algorithm
    ; WITH
    RowSet0 as (select 1 as Item union all select 1),              --          2 rows   -- We only have to name the column in the first select, the second/union select inherits the column name
    RowSet1 as (select 1 as Item from RowSet0 as A, RowSet0 as B), --          4 rows
    RowSet2 as (select 1 as Item from RowSet1 as A, RowSet1 as B), --         16 rows
    RowSet3 as (select 1 as Item from RowSet2 as A, RowSet2 as B), --        256 rows
    RowSet4 as (select 1 as Item from RowSet3 as A, RowSet3 as B), --      65536 rows (65k)
    RowSet5 as (select 1 as Item from RowSet4 as A, RowSet4 as B), -- 4294967296 rows (4BB)
    -- Add more RowSetX to get higher and higher numbers of rows    
    -- Each successive RowSetX results in squaring the previously available number of rows
    Tally   as (select row_number() over (order by Item) as Number from RowSet5) -- This is what gives us the sequence of integers, always select from the terminal CTE expression
    -- Note: testing of this specific use case has shown that making Tally as a sub-query instead of a terminal CTE expression is slower (always) - be sure to follow this pattern closely for max performance
    INSERT INTO [Numbers].[Test] (Number)
    SELECT o.Number
    FROM Tally o
    WHERE o.Number <= @NumberOfNumbers

    ALTER TABLE [Numbers].[Test] ADD CONSTRAINT PK_Numbers_Test_Number PRIMARY KEY CLUSTERED (Number)

    -- Record the end of the test
    UPDATE tt
        SET 
            EndDate = GETDATE()
    FROM #TimingTest tt
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    -- And the stats about the numbers in the sequence
    UPDATE tt
        SET 
            ItemCount = results.ItemCount,
            MaxNumber = results.MaxNumber,
            MinNumber = results.MinNumber
    FROM #TimingTest tt
    CROSS JOIN (
        SELECT COUNT(Number) as ItemCount, MAX(Number) as MaxNumber, MIN(Number) as MinNumber FROM [Numbers].[Test]
    ) results
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    --
    -- METHOD - Mladen Prajdic answer
    --

    -- Prep for the test
    DROP TABLE IF EXISTS [Numbers].[Test];
    CREATE TABLE [Numbers].[Test] (Number INT NOT NULL);

    -- Method information
    SET @MethodName = 'MladenPrajdic';

    -- Record the start of the test
    INSERT INTO #TimingTest(MethodName, TestIteration, StartDate)
    SELECT @MethodName, @TestIteration, GETDATE()

    -- Run the algorithm
    INSERT INTO [Numbers].[Test](Number)
    SELECT TOP (@NumberOfNumbers) row_number() over(order by t1.number) as N
    FROM master..spt_values t1 
    CROSS JOIN master..spt_values t2

    ALTER TABLE [Numbers].[Test] ADD CONSTRAINT PK_Numbers_Test_Number PRIMARY KEY CLUSTERED (Number)

    -- Record the end of the test
    UPDATE tt
        SET 
            EndDate = GETDATE()
    FROM #TimingTest tt
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    -- And the stats about the numbers in the sequence
    UPDATE tt
        SET 
            ItemCount = results.ItemCount,
            MaxNumber = results.MaxNumber,
            MinNumber = results.MinNumber
    FROM #TimingTest tt
    CROSS JOIN (
        SELECT COUNT(Number) as ItemCount, MAX(Number) as MaxNumber, MIN(Number) as MinNumber FROM [Numbers].[Test]
    ) results
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    --
    -- METHOD - Single INSERT
    -- 

    -- Prep for the test
    DROP TABLE IF EXISTS [Numbers].[Test];
    -- The Table creation is part of this algorithm ...

    -- Method information
    SET @MethodName = 'SingleInsert';

    -- Record the start of the test
    INSERT INTO #TimingTest(MethodName, TestIteration, StartDate)
    SELECT @MethodName, @TestIteration, GETDATE()

    -- Run the algorithm
    SELECT TOP (@NumberOfNumbers) IDENTITY(int,1,1) AS Number
    INTO [Numbers].[Test]
    FROM sys.objects s1       -- use sys.columns if you don't get enough rows returned to generate all the numbers you need
    CROSS JOIN sys.objects s2 -- use sys.columns if you don't get enough rows returned to generate all the numbers you need

    ALTER TABLE [Numbers].[Test] ADD CONSTRAINT PK_Numbers_Test_Number PRIMARY KEY CLUSTERED (Number)

    -- Record the end of the test
    UPDATE tt
        SET 
            EndDate = GETDATE()
    FROM #TimingTest tt
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration

    -- And the stats about the numbers in the sequence
    UPDATE tt
        SET 
            ItemCount = results.ItemCount,
            MaxNumber = results.MaxNumber,
            MinNumber = results.MinNumber
    FROM #TimingTest tt
    CROSS JOIN (
        SELECT COUNT(Number) as ItemCount, MAX(Number) as MaxNumber, MIN(Number) as MinNumber FROM [Numbers].[Test]
    ) results
    WHERE tt.MethodName = @MethodName
    and tt.TestIteration = @TestIteration
END

-- Calculate the timespan for each of the runs
UPDATE tt
    SET
        ElapsedTime = DATEDIFF(MICROSECOND, StartDate, EndDate)
FROM #TimingTest tt

--
-- Report the results ...
--
SELECT 
    MethodName, AVG(ElapsedTime) / AVG(ItemCount) as TimePerRecord, CAST(AVG(ItemCount) as bigint) as SequenceLength,
    MAX(ElapsedTime) as MaxTime, MIN(ElapsedTime) as MinTime,
    MAX(MaxNumber) as MaxNumber, MIN(MinNumber) as MinNumber
FROM #TimingTest tt
GROUP by tt.MethodName
ORDER BY TimePerRecord ASC, MaxTime ASC, MinTime ASC
Другие вопросы по тегам