Самый быстрый способ разбить / сохранить длинную строку для функции charindex

8

У меня есть строка цифр 1 ТБ. Учитывая 12-символьную последовательность цифр, я хочу получить начальную позицию этой последовательности в исходной строке ( charindexфункции).

Я проверил это со строкой 1 ГБ и подстрокой из 9 цифр, используя SQL Server, сохраняя строку как varchar(max). Charindexзанимает 10 секунд. Разбиение строки размером 1 ГБ в 900-байтовых перекрывающихся чанках и создание таблицы (StartPositionOfChunk, Chunkofstring) с chunkofstring в двоичном сопоставлении, для индексации занимает менее 1 секунды. Последний метод для 10GB, 10-значная подстрока поднимает charindex до 1,5 мин. Я хотел бы найти более быстрый способ хранения.

пример

строка цифр: 0123456789 - подстрока для поиска 345
charindex ('345', '0123456789') дает 4

Способ 1 : теперь я могу сохранить это в табличной таблице SQL Server, состоящей из одного столбца, colstrи выполнить:

select charindex('345',colstr) from strtable

Способ 2 : или я могу составить таблицу strtable2 (pos, colstr1) , разделив исходную строку: 1; 012 | 2; 123 | 3; 234 aso и тогда мы можем получить запрос

select pos from strtable2 where colstr1='345'

Способ 3 : я могу составить таблицу strtable2 (pos2, colstr2) , разделив исходную строку на более крупные куски 1; 01234 | 4; 34567 | 7; 6789, а затем

select pos2+charindex('345',colstr2) from strtable2 where colstr2 like '%345%'

Первый метод самый медленный.

Второй метод увеличивает размер хранилища базы данных!

Метод 3 : Установка длины colstr2 равной 900 байтов в двоичной разборке, создание индекса для этого столбца занимает 1 секунду для поиска строки 1 ГБ и поиска подстроки из 9 цифр. Для строки длиной 10 ГБ и подстроки из 10 цифр истечение 90 секунд.

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

Поиск всегда выполняется для 12-значной подстроки в строке размером 1 ТБ SQL Server 2017 Developer Edition, 16 ядер, 16 ГБ ОЗУ. Основная цель - скорость поиска! 10 цифр в строке 10 ГБ (для тестирования производительности).

Вернер Аумайр
источник

Ответы:

6

Я рекомендую использовать разновидность метода 2 и разбить диапазон поиска на множество целевых таблиц. 10000 столов - хорошая первая попытка. Например, если вы ищете «012345678901», тогда ваш запрос будет искать в таблице, связанной с данными, которые начинаются с «0123». Всего будет по-прежнему триллион строк, но разделение данных на множество таблиц имеет следующие положительные качества:

  1. Все возможные 12-значные строки теперь могут помещаться в INT.
  2. Создание более эффективного поискового представления вашей строки размером 1 ТБ, вероятно, будет дорогостоящим, несмотря ни на что. Имея множество таблиц, вы можете легко распараллелить работу и даже временно запросить выделение большего количества процессоров для вашей виртуальной машины.
  3. Вы можете создать отдельную таблицу в качестве доказательства концепции для определения времени запроса и требований к общему пространству для полной строки.
  4. Если вам когда-нибудь понадобится какое-либо обслуживание базы данных, вы будете рады, что не создали одну огромную таблицу.

На данный момент главный вопрос для меня заключается в том, используете ли вы сжатое хранилище строк или columnstore. Приведенный ниже код создает таблицу хранилища строк для пространства поиска «0123» и вставляет в нее 100 миллионов строк. Если ваша строка достаточно случайная, вы также можете увидеть около 100 миллионов строк в таблице.

DROP TABLE IF EXISTS #t;

SELECT TOP (10000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.Q229892_RAW_100M_RANGE;

CREATE TABLE dbo.Q229892_RAW_100M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_100M_RANGE WITH (TABLOCK)
SELECT ABS(CHECKSUM(NEWID()) % 100000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM #t t1
CROSS JOIN #t t2
OPTION (MAXDOP 4);


DROP TABLE IF EXISTS dbo.T0123_Q229892_PAGE_COMPRESSION;

CREATE TABLE dbo.T0123_Q229892_PAGE_COMPRESSION (
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    PRIMARY KEY (STRING_PIECE, STR_POS)
) WITH (DATA_COMPRESSION = PAGE);

INSERT INTO dbo.T0123_Q229892_PAGE_COMPRESSION WITH (TABLOCK)
SELECT STRING_PIECE, STR_POS
FROM dbo.Q229892_RAW_100M_RANGE;

Плохая новость - для полного набора данных вам, вероятно, потребуется около 15,4 ТБ. Хорошей новостью является то, что запросы занимают у меня всего 1 мс, даже если в буферном кеше нет соответствующих данных, что почти всегда будет иметь место для такого большого набора данных, как ваш.

-- 1 ms
CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_PAGE_COMPRESSION
WHERE STRING_PIECE = 45678901; -- searching for '012345678901'

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

Что касается columnstore, вы не можете искать нужные вам данные, и все равно крайне маловероятно, что вы поместите все свои данные в память, поэтому важно читать как можно меньше сжатых данных в ваших запросах. Я настоятельно рекомендую разделить ваши таблицы. Один простой подход, который хорошо работает, состоит в том, чтобы использовать первые четыре цифры строки поиска, чтобы найти имя таблицы и следующие две цифры в качестве раздела. Снова используя «012345678901», вы перейдете к разделу 45 таблицы, в которой содержатся данные для «0123». 100 разделов - это хорошее число, чтобы избежать проблем, вызванных слишком большим количеством разделов, и в среднем у вас будет около 1 миллиона строк на каждый раздел. Максимальное количество строк, которое может поместиться в одну группу строк, составляет 1048576, поэтому при таком подходе вы будете выполнять как можно меньше операций ввода-вывода.

DROP TABLE IF EXISTS dbo.Q229892_RAW_1M_RANGE;

CREATE TABLE dbo.Q229892_RAW_1M_RANGE (
STRING_PIECE INT NOT NULL,
STR_POS BIGINT NOT NULL
);

INSERT INTO dbo.Q229892_RAW_1M_RANGE WITH (TABLOCK)
SELECT TOP (1000000) ABS(CHECKSUM(NEWID()) % 1000000),
TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT) * TRY_CAST(ABS(CHECKSUM(NEWID())) AS BIGINT)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);



DECLARE @IntegerPartitionFunction nvarchar(max) = 
    N'CREATE PARTITION FUNCTION partition100 (tinyint) 
    AS RANGE LEFT FOR VALUES (';  
DECLARE @i int = 0;  
WHILE @i < 100
BEGIN  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N', ';  
SET @i += 1;  
END  
SET @IntegerPartitionFunction += CAST(@i as nvarchar(10)) + N');';  
EXEC sp_executesql @IntegerPartitionFunction;  
GO  

CREATE PARTITION SCHEME partition100_scheme
AS PARTITION partition100  
ALL TO ([DEFAULT]);

DROP TABLE IF EXISTS dbo.T0123_Q229892_COLUMNSTORE;

-- this table must be partitioned by PART_ID!
CREATE TABLE dbo.T0123_Q229892_COLUMNSTORE (
    PART_ID TINYINT NOT NULL,
    STRING_PIECE INT NOT NULL,
    STR_POS BIGINT NOT NULL,
    INDEX CCS CLUSTERED COLUMNSTORE
) ON partition100_scheme (PART_ID);


GO

DECLARE @part_id TINYINT = 0;
SET NOCOUNT ON;
WHILE @part_id < 100
BEGIN
    INSERT INTO dbo.T0123_Q229892_COLUMNSTORE WITH (TABLOCK)
    SELECT @part_id, STRING_PIECE, STR_POS
    FROM dbo.Q229892_RAW_1M_RANGE
    OPTION (MAXDOP 1);

    SET @part_id = @part_id + 1;
END;

GO

При таком подходе полный набор данных потребует около 10,9 ТБ. Мне не понятно, как сделать это меньше. В этом случае поисковый запрос немного медленнее. На моей машине это занимает около 25 мс, но это в основном зависит от ввода-вывода:

CHECKPOINT;
DBCC DROPCLEANBUFFERS;

SELECT MIN(STR_POS)
FROM dbo.T0123_Q229892_COLUMNSTORE
WHERE PART_ID = 45
AND STRING_PIECE = 678901; -- searching for '012345678901'

Одно важное замечание о подходе columnstore заключается в том, что показатель 10,9 ТБ предназначен для 100% сжатых данных. Будет сложно заполнить такую ​​таблицу эффективно, избегая при этом магазинов дельты. Вполне вероятно, что в какой-то момент процесса вы получите несжатые данные в дельта-хранилищах, для которых может потребоваться больше, чем 15,4 ТБ, использованных для хранилища строк.

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

Хранение и обработка 1 ТБ данных при наличии только 16 ГБ ОЗУ может оказаться проблемой. 1 ГБ на ядро ​​довольно несбалансированно, особенно для такой нагрузки. 8 ГБ на ядро ​​будет гораздо лучшей отправной точкой, причем более желательной.

Тем не менее, я все еще хотел бы попробовать вариант 2:

Сохраните все возможные 12-символьные подстроки, как bigintв кластерной таблице columnstore (с архивным сжатием, если это окажется полезным):

CREATE TABLE dbo.Search
(
    pos bigint NOT NULL,
    fragment bigint NOT NULL,

    INDEX CCS CLUSTERED COLUMNSTORE 
        WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE) -- optional
);

Возможно, вам придется реализовать какой-то способ постепенной загрузки исходных данных в эту таблицу. Убедитесь, что у вас есть группы строк максимального размера (1 048 576 строк) в готовой структуре columnstore . Смотрите руководство по загрузке данных .

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

С этим методом возможна очень хорошая производительность, но, как это часто бывает с columnstore, вам необходимо добиться эффективного удаления разделов и сегментов. Разделение fragmentстолбца и последовательное построение индекса columnstore при замене кластеризованного индекса rowstore fragment- это способ достижения этого, как отмечено в документации, приведенной выше. Это также минимизирует потребность в хранении, поскольку fragmentзначения в одном и том же диапазоне будут храниться в одном и том же сегменте. Это позволяет эффективно изменять значения и упаковывать биты.

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

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

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