Почему таблицы чисел «бесценны»?

112

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

USE Model
GO

CREATE TABLE Numbers
(
    Number INT NOT NULL,
    CONSTRAINT PK_Numbers 
        PRIMARY KEY CLUSTERED (Number)
        WITH FILLFACTOR = 100
)

INSERT INTO Numbers
SELECT
    (a.Number * 256) + b.Number AS Number
FROM 
    (
        SELECT number
        FROM master..spt_values
        WHERE 
            type = 'P'
            AND number <= 255
    ) a (Number),
    (
        SELECT number
        FROM master..spt_values
        WHERE 
            type = 'P'
            AND number <= 255
    ) b (Number)
GO

Согласно сообщению в блоге, обоснование

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

Но я не совсем понимаю, что это за использование - можете ли вы привести некоторые убедительные, конкретные примеры того, как «таблица чисел» экономит вам массу работы в SQL Server - и почему мы должны их иметь?

Джефф Этвуд
источник
3
Многие варианты использования для таблицы чисел могут быть одинаково удовлетворены рекурсивным CTE, который генерирует числа, которые вам нужны на лету. Тем не менее, существует снижение производительности, а также некоторые другие ограничения в подходе CTE.
Ник Чаммас
4
@Nick: Я бы сказал, что таблица номеров, основанная на CTE «на лету», и физическая таблица - это просто детали реализации того, как вы генерируете таблицу чисел. Картофель против картофеля ...
Ремус Русану
1
@Remus - Да. Я просто хотел указать на эту альтернативу Джеффу.
Ник Чаммас
2
У меня есть дюжина ответов с использованием таблицы чисел на SO stackoverflow.com/search?q=user%3A27535+%2B%22numbers+table%22 .
ГБН

Ответы:

82

Я видел много случаев, когда вам нужно проецировать «недостающие данные». Например. у вас есть временной ряд (например, журнал доступа), и вы хотите показать количество посещений в день за последние 30 дней (панель инструментов аналитики). Если вы сделаете a, select count(...) from ... group by dayвы получите счет за каждый день, но результат будет содержать только строку за каждый день, когда у вас был хотя бы один доступ. С другой стороны, если вы сначала проецируете таблицу дней из таблицы чисел ( select dateadd(day, -number, today) as day from numbers), а затем оставляете объединение со счетчиками (или внешним применением, как вам нравится), то вы получите результат с 0 для количества дней, которые вы не было доступа. Это только один пример. Конечно, можно утверждать, что уровень представления вашей информационной панели может обрабатывать пропущенные дни и просто отображать 0, но некоторые инструменты (например, SSRS) просто не смогут справиться с этим.

Другие примеры, которые я видел, использовали аналогичные трюки с временными рядами (дата / время +/- число) для выполнения всех видов оконных вычислений. В общем, всякий раз, когда в императивном языке вы используете цикл for с хорошо известным числом итераций, декларативный и заданный характер SQL может использовать прием, основанный на таблице чисел.

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

int x;
for (int i=0;i<1000000;++i)
  x = i;
printf("%d",x);

Эта программа выдаст 999999, что в значительной степени гарантировано.

Давайте попробуем то же самое в SQL Server, используя таблицу чисел. Сначала создайте таблицу из 1 000 000 номеров:

create table numbers (number int not null primary key);
go

declare @i int = 0
    , @j int = 0;

set nocount on;
begin transaction
while @i < 1000
begin
    set @j = 0;
    while @j < 1000
    begin
        insert into numbers (number) 
            values (@j*1000+@i);
        set @j += 1;
    end
    commit;
    raiserror (N'Inserted %d*1000', 0, 0, @i)
    begin transaction;
    set @i += 1;
end
commit
go

Теперь давайте сделаем цикл for:

declare @x int;
select @x = number 
from numbers with(nolock);
select @x as [@x];

Результат:

@x
-----------
88698

Если у вас сейчас есть момент WTF (в конце концов, number это кластерный первичный ключ!), Этот трюк называется сканированием порядка распределения, и я не вставил @j*1000+@iего случайно ... Вы могли бы также рискнуть и сказать, что результат состоит в том, что параллелизм и это иногда может быть правильным ответом.

Под этим мостом много троллей, и я упомянул некоторые из них в « Булевых операторах на SQL Server», и функции T-SQL не предполагают определенного порядка выполнения.

Ремус Русану
источник
55

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

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

  • Разбор строки
  • Нахождение пробелов в идентичности
  • Генерация диапазонов дат (например, заполнение календарной таблицы, которая также может быть неоценимой)
  • Генерация временных интервалов
  • Генерация диапазонов IP

В разделе «Плохие привычки»: используя циклы для заполнения больших таблиц , я показываю, как можно использовать таблицу чисел для быстрой вставки большого количества строк (в отличие от подхода коленного рефлекса с использованием цикла while).

В разделе «Обработка списка целых чисел: мой подход» и « Подробнее о разделении списков: настраиваемые разделители, предотвращение дублирования и поддержание порядка» я показываю, как использовать таблицу чисел для разделения строки (например, набора значений, разделенных запятыми) и обеспечения производительности. Сравнение между этим и другими методами. Больше информации о разделении и обработке других строк:

И в Таблице чисел SQL Server, объяснение - часть 1 , я даю некоторое представление о концепции и будущие публикации в запасе, чтобы детализировать конкретные приложения.

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

И, как и @gbn, у меня есть несколько ответов о переполнении стека и на этом сайте, которые также используют таблицу чисел.

Наконец, у меня есть серия постов в блоге о создании наборов без зацикливания, которые частично показывают преимущество в производительности при использовании таблицы чисел по сравнению с большинством других методов (за исключением странного выброса Ремуса):

Аарон Бертран
источник
26

Вот прекрасный пример, который я недавно использовал от Адама Мачаника:

CREATE FUNCTION dbo.GetSubstringCount
(
    @InputString TEXT, 
    @SubString VARCHAR(200),
    @NoisePattern VARCHAR(20)
)
RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
    RETURN 
    (
        SELECT COUNT(*)
        FROM dbo.Numbers N
        WHERE
            SUBSTRING(@InputString, N.Number, LEN(@SubString)) = @SubString
            AND PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number + LEN(@SubString), 1)) = 0
            AND 0 = 
                CASE 
                    WHEN @NoisePattern = '' THEN 0
                    ELSE PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number - 1, 1))
                END
    )
END

Я использовал нечто похожее с a, CTEчтобы найти конкретный экземпляр подстроки (например, «Найти 3-й канал в этой строке») для работы с коррелированными данными с разделителями:

declare @TargetStr varchar(8000), 
@SearchedStr varchar(8000), 
@Occurrence int
set @TargetStr='a'
set @SearchedStr='abbabba'
set @Occurrence=3;

WITH Occurrences AS (
SELECT Number,
       ROW_NUMBER() OVER(ORDER BY Number) AS Occurrence
FROM master.dbo.spt_values
WHERE Number BETWEEN 1 AND LEN(@SearchedStr) AND type='P'
  AND SUBSTRING(@SearchedStr,Number,LEN(@TargetStr))=@TargetStr)
SELECT Number
FROM Occurrences
WHERE Occurrence=@Occurrence

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

JNK
источник
5
И обязательное предупреждение о скрытой опасности выполнения строковых манипуляций во встроенных TVF: функции T-SQL не подразумевают определенного порядка выполнения
Remus Rusanu