Оптимізація пошуку чисельного діапазону (інтервалу) пошуку в SQL Server


18

Це питання схоже на Оптимізація пошуку в IP-діапазоні? але це обмежено для SQL Server 2000.

Припустимо, у мене є 10 мільйонів діапазонів, що попередньо зберігаються в таблиці, структурованій та заповненій, як показано нижче.

CREATE TABLE MyTable
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX IX1 (RangeFrom,RangeTo),
INDEX IX2 (RangeTo,RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO MyTable
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(1)
FROM   RandomNumbers 

Мені потрібно знати всі діапазони, що містять значення 50,000,000. Я намагаюся наступний запит

SELECT *
FROM MyTable
WHERE 50000000 BETWEEN RangeFrom AND RangeTo

SQL Server показує, що було 10951 логічне читання і майже 5 мільйонів рядків було прочитано, щоб повернути 12 відповідних.

введіть тут опис зображення

Чи можу я покращити цей показник? Будь-яка реструктуризація таблиці або додаткових індексів добре.


Якщо я правильно розумію налаштування таблиці, ви вибираєте випадкові числа рівномірно, щоб формувати свої діапазони, без обмежень щодо "розміру" кожного діапазону. А ваш зонд на середину загального діапазону 1..100М. У цьому випадку - відсутність явної кластеризації через однакову випадковість - я не знаю, чому індекс на нижній чи верхній межі був би корисним. Чи можете ви пояснити це?
davidbak

@davidbak звичайні індекси в цій таблиці справді не дуже корисні в гіршому випадку, оскільки він повинен сканувати половину діапазону, отже, вимагаючи можливих удосконалень. Існує приємне поліпшення пов'язаного питання для SQL Server 2000 із введенням "гранули", що я сподівався, що просторові індекси можуть допомогти тут, оскільки вони підтримують containsзапити, і в той час як вони добре працюють при зменшенні кількості прочитаних даних, вони, здається, додають інші накладні витрати, які протидіють цьому.
Мартін Сміт

У мене немає можливості спробувати це - але мені цікаво, чи два індекси - один на нижній межі, один на верхній, а потім внутрішній приєднання - дозволять оптимізатору запитів щось розробити.
davidbak

Відповіді:


11

Columnstore тут дуже хиткий порівняно з некластеризованим індексом, який сканує половину таблиці. Некластерний індекс стовпців стовпців забезпечує більшу частину переваг, але введення впорядкованих даних у кластерний індекс стовпців стовпців ще краще.

DROP TABLE IF EXISTS dbo.MyTableCCI;

CREATE TABLE dbo.MyTableCCI
(
Id        INT PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom),
INDEX CCI CLUSTERED COLUMNSTORE
);

INSERT INTO dbo.MyTableCCI
SELECT TOP (987654321) *
FROM dbo.MyTable
ORDER BY RangeFrom ASC
OPTION (MAXDOP 1);

За дизайном я можу отримати усунення груп рядків на RangeFromстовпчику, який видалить половину моїх груп рядків. Але через характер даних я також отримую усунення груп рядків у RangeToстовпці:

Table 'MyTableCCI'. Segment reads 1, segment skipped 9.

Для більших таблиць із більш змінними даними існують різні способи завантаження даних, щоб гарантувати найкраще можливе усунення груп рядків у обох стовпцях. Зокрема, для ваших даних запит займає 1 мс.


так, безумовно, шукають інші підходи, які слід розглядати без обмеження 2000 року. Не звучить так, що це буде бито.
Мартін Сміт

9

Пол Уайт вказав на відповідь на подібне запитання, що містить посилання на цікаву статтю Іціка Бен Гана . Це описує модель "Статичного реляційного інтервального дерева", яка дозволяє це зробити ефективно.

Підсумовуючи підсумок, цей підхід передбачає збереження обчисленого ("forknode") значення на основі інтервальних значень у рядку. Під час пошуку діапазонів, що перетинають інший діапазон, можна попередньо підрахувати можливі значення forknode, які повинні мати відповідні рядки, і використовувати це для пошуку результатів максимум 31 операцією пошуку (нижче наведено цілі числа в діапазоні від 0 до максимально підписаного 32 біт int)

На основі цього я реструктурував таблицю, як показано нижче.

CREATE TABLE dbo.MyTable3
(
  Id        INT IDENTITY PRIMARY KEY,
  RangeFrom INT NOT NULL,
  RangeTo   INT NOT NULL,   
  node  AS RangeTo - RangeTo % POWER(2, FLOOR(LOG((RangeFrom - 1) ^ RangeTo, 2))) PERSISTED NOT NULL,
  CHECK (RangeTo > RangeFrom)
);

CREATE INDEX ix1 ON dbo.MyTable3 (node, RangeFrom) INCLUDE (RangeTo);
CREATE INDEX ix2 ON dbo.MyTable3 (node, RangeTo) INCLUDE (RangeFrom);

SET IDENTITY_INSERT MyTable3 ON

INSERT INTO MyTable3
            (Id,
             RangeFrom,
             RangeTo)
SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable

SET IDENTITY_INSERT MyTable3 OFF 

А потім використовується наступний запит (стаття шукає пересічні інтервали, тому пошук інтервалу, що містить точку, є виродженим випадком цього)

DECLARE @value INT = 50000000;

;WITH N AS
(
SELECT 30 AS Level, 
       CASE WHEN @value > POWER(2,30) THEN POWER(2,30) END AS selected_left_node, 
       CASE WHEN @value < POWER(2,30) THEN POWER(2,30) END AS selected_right_node, 
       (SIGN(@value - POWER(2,30)) * POWER(2,29)) + POWER(2,30)  AS node
UNION ALL
SELECT N.Level-1,   
       CASE WHEN @value > node THEN node END AS selected_left_node,  
       CASE WHEN @value < node THEN node END AS selected_right_node,
       (SIGN(@value - node) * POWER(2,N.Level-2)) + node  AS node
FROM N 
WHERE N.Level > 0
)
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS L
    ON I.node = L.selected_left_node
    AND I.RangeTo >= @value
    AND L.selected_left_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
  JOIN N AS R
    ON I.node = R.selected_right_node
    AND I.RangeFrom <= @value
    AND R.selected_right_node IS NOT NULL
UNION ALL
SELECT I.id, I.RangeFrom, I.RangeTo
FROM dbo.MyTable3 AS I
WHERE node = @value;

Зазвичай це виконується в 1ms на моїй машині, коли всі сторінки знаходяться в кеші - зі статистикою IO.

Table 'MyTable3'. Scan count 24, logical reads 72, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 4, logical reads 374, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

і планувати

введіть тут опис зображення

NB: Джерело використовує багатоступеневі телевізійні канали, а не рекурсивний CTE для отримання вузлів для приєднання, але в інтересах зробити свою відповідь самодостатньою я вибрав останнє. Для виробництва я б, ймовірно, використовував телевізори.


9

Мені вдалося знайти підхід до режиму рядка, який є конкурентоспроможним підходу N / CCI, але вам потрібно знати щось про ваші дані. Припустимо , що у вас є стовпець , який містив різницю RangeFromі RangeToі ви індексується його разом з RangeFrom:

ALTER TABLE dbo.MyTableWithDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableWithDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

Якби ви знали всі різні значення, DiffOfColumnsтоді ви могли б здійснити пошук кожного значення DiffOfColumnsіз фільтром діапазону, RangeToщоб отримати всі відповідні дані. Наприклад, якщо ми знаємо, що DiffOfColumns= 2, то єдиними дозволеними значеннями RangeFromє 49999998, 49999999 та 50000000. Рекурсія може бути використана для отримання всіх відмінних значень, DiffOfColumnsі це добре працює для вашого набору даних, оскільки їх всього 256. Наведений нижче запит на моїй машині займає близько 6 мс:

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        DiffOfColumns
    FROM dbo.MyTableWithDiff AS T
    ORDER BY
        T.DiffOfColumns

    UNION ALL

    -- Recursive
    SELECT R.DiffOfColumns
    FROM
    (
        -- Number the rows
        SELECT 
            T.DiffOfColumns,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.DiffOfColumns)
        FROM dbo.MyTableWithDiff AS T
        JOIN RecursiveCTE AS R
            ON R.DiffOfColumns < T.DiffOfColumns
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT ca.*
FROM RecursiveCTE rcte
CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableWithDiff mt
    WHERE mt.DiffOfColumns = rcte.DiffOfColumns
    AND mt.RangeFrom >= 50000000 - rcte.DiffOfColumns AND mt.RangeFrom <= 50000000
) ca
OPTION (MAXRECURSION 0);

Ви можете бачити звичайну рекурсивну частину разом з пошуком індексу для кожного окремого значення:

план запитів 1

Недолік цього підходу полягає в тому, що він починає повільно ставати, коли занадто багато різних значень для DiffOfColumns. Давайте зробимо те саме тест, але використовуйте CRYPT_GEN_RANDOM(2)замість CRYPT_GEN_RANDOM(1).

DROP TABLE IF EXISTS dbo.MyTableBigDiff;

CREATE TABLE dbo.MyTableBigDiff
(
Id        INT IDENTITY PRIMARY KEY,
RangeFrom INT NOT NULL,
RangeTo   INT NOT NULL,
CHECK (RangeTo > RangeFrom)
);

WITH RandomNumbers
     AS (SELECT TOP 10000000 ABS(CRYPT_GEN_RANDOM(4)%100000000) AS Num
         FROM   sys.all_objects o1,
                sys.all_objects o2,
                sys.all_objects o3,
                sys.all_objects o4)
INSERT INTO dbo.MyTableBigDiff
            (RangeFrom,
             RangeTo)
SELECT Num,
       Num + 1 + CRYPT_GEN_RANDOM(2) -- note the 2
FROM   RandomNumbers;


ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns AS RangeTo-RangeFrom;

CREATE INDEX IXDIFF ON dbo.MyTableBigDiff (DiffOfColumns,RangeFrom) INCLUDE (RangeTo);

Цей же запит тепер знаходить 65536 рядків з рекурсивної частини і займає 823 мс процесора на моїй машині. Там чекає PAGELATCH_SH та інші погані речі. Я можу підвищити продуктивність, використовуючи розрив значень різниці, щоб тримати під контролем кількість унікальних значень та коригувати для кодування CROSS APPLY. Для цього набору даних я спробую 256 відра:

ALTER TABLE dbo.MyTableBigDiff ADD DiffOfColumns_bucket256 AS CAST(CEILING((RangeTo-RangeFrom) / 256.) AS INT);

CREATE INDEX [IXDIFF😎] ON dbo.MyTableBigDiff (DiffOfColumns_bucket256, RangeFrom) INCLUDE (RangeTo);

Один із способів уникнути отримання зайвих рядків (зараз я порівнюю із округленим значенням замість справжнього значення) - фільтруючи RangeTo:

CROSS APPLY (
    SELECT mt.Id, mt.RangeFrom, mt.RangeTo
    FROM dbo.MyTableBigDiff mt
    WHERE mt.DiffOfColumns_bucket256 = rcte.DiffOfColumns_bucket256
    AND mt.RangeFrom >= 50000000 - (256 * rcte.DiffOfColumns_bucket256)
    AND mt.RangeFrom <= 50000000
    AND mt.RangeTo >= 50000000
) ca

Повний запит займає 6 мс на моїй машині.


8

Одним із альтернативних способів представлення діапазону було б вказування точок на лінії.

Нижче перенесені всі дані в нову таблицю з діапазоном, представленим у вигляді geometryтипу даних.

CREATE TABLE MyTable2
(
Id INT IDENTITY PRIMARY KEY,
Range GEOMETRY NOT NULL,
RangeFrom AS Range.STPointN(1).STX,
RangeTo   AS Range.STPointN(2).STX,
CHECK (Range.STNumPoints() = 2 AND Range.STPointN(1).STY = 0 AND Range.STPointN(2).STY = 0)
);

SET IDENTITY_INSERT MyTable2 ON

INSERT INTO MyTable2
            (Id,
             Range)
SELECT ID,
       geometry::STLineFromText(CONCAT('LINESTRING(', RangeFrom, ' 0, ', RangeTo, ' 0)'), 0)
FROM   MyTable

SET IDENTITY_INSERT MyTable2 OFF 


CREATE SPATIAL INDEX index_name   
ON MyTable2 ( Range )  
USING GEOMETRY_GRID  
WITH (  
BOUNDING_BOX = ( xmin=0, ymin=0, xmax=110000000, ymax=1 ),  
GRIDS = (HIGH, HIGH, HIGH, HIGH),  
CELLS_PER_OBJECT = 16); 

Еквівалентний запит на пошук діапазонів, що містять значення, 50,000,000знаходиться нижче.

SELECT Id,
       RangeFrom,
       RangeTo
FROM   MyTable2
WHERE  Range.STContains(geometry::STPointFromText ('POINT (50000000 0)', 0)) = 1 

Читання для цього показують покращення щодо 10,951вихідного запиту.

Table 'MyTable2'. Scan count 0, logical reads 505, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'extended_index_1797581442_384000'. Scan count 4, logical reads 17, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Однак суттєвого поліпшення в порівнянні з оригіналом за часом, що минув . Типові результати виконання - 250 мс проти 252 мс.

План виконання є складнішим, як показано нижче

введіть тут опис зображення

Єдиний випадок, коли перезапис надійно працює для мене - це холодний кеш.

Тож невтішно в цьому випадку і важко рекомендувати це перезапис, але публікація негативних результатів також може бути корисною.


5

Як шанування наших нових роботів-оверлодів, я вирішив побачити, чи може хтось із нових функцій R та Python допомогти нам тут. Відповідь - ні, принаймні для сценаріїв, над якими я міг би працювати і повертати правильні результати. Якщо хтось з кращими знаннями прийде разом, то, не соромтесь поплескувати мене. Мої ставки розумні.

Для цього я встановив VM з 4 ядрами та 16 Гб оперативної пам’яті, думаючи, що цього буде достатньо для вирішення набору даних ~ 200 Мб.

Почнемо з мови, якої не існує в Бостоні!

R

EXEC sp_execute_external_script 
@language = N'R', 
@script = N'
tweener = 50000000
MO = data.frame(MartinIn)
MartinOut <- subset(MO, RangeFrom <= tweener & RangeTo >= tweener, select = c("Id","RangeFrom","RangeTo"))
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

Це був поганий час.

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3219 ms,  elapsed time = 5349 ms.

План виконання досить нецікавий, хоча я не знаю, чому середній оператор повинен називати нас іменами.

Горіхи

Далі, кодування олівцями!

Пітон

EXEC sp_execute_external_script 
@language = N'Python', 
@script = N'
import pandas as pd
MO = pd.DataFrame(MartinIn)
tweener = 50000000
MartinOut = MO[(MO.RangeFrom <= tweener) & (MO.RangeTo >= tweener)]
', 
@input_data_1_name = N'MartinIn',
@input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable',
@output_data_1_name = N'MartinOut',
@parallel = 1
WITH RESULT SETS ((ID INT, RangeFrom INT, RangeTo INT));

Тільки коли ви думали, що він не може бути гіршим за R:

Table 'MyTable'. Scan count 1, logical reads 22400

 SQL Server Execution Times:
   CPU time = 3797 ms,  elapsed time = 10146 ms.

Ще один план безладного виконання :

Горіхи

Хм і хммер

Поки що я не вражений. Я не можу зачекати, щоб видалити цей ВМ.


1
Ви також можете передавати параметри, наприклад, DECLARE @input INT = 50000001; EXEC dbo.sp_execute_external_script @language = N'R', @script = N'OutputDataSet <- InputDataSet[which(x >= InputDataSet$RangeFrom & x <= InputDataSet$RangeTo) , ]', @parallel = 1, @input_data_1 = N'SELECT Id, RangeFrom, RangeTo FROM dbo.MyTable;', @params = N'@x INT', @x = 50000001 WITH RESULT SETS ( ( Id INT NOT NULL, RangeFrom INT NOT NULL, RangeTo INT NOT NULL ));але так, ефективність не велика. Я використовую R для речей, які ви не можете зробити в SQL, скажіть, якщо ви хочете щось передбачити.
wBob

4

Я знайшов досить вдале рішення, використовуючи обчислений стовпець, однак це добре лише для одного значення. Якщо говорити, якщо у вас є магічне значення, можливо, цього достатньо.

Починаючи з даного зразка, потім змінюючи таблицю:

ALTER TABLE dbo.MyTable
    ADD curtis_jackson 
        AS CONVERT(BIT, CASE 
                            WHEN RangeTo >= 50000000
                            AND RangeFrom < 50000000
                            THEN 1 
                            ELSE 0 
                        END);

CREATE INDEX IX1_redo 
    ON dbo.MyTable (curtis_jackson) 
        INCLUDE (RangeFrom, RangeTo);

Запит просто стає:

SELECT *
FROM MyTable
WHERE curtis_jackson = 1;

Який повертає ті самі результати, що і ваш початковий запит. З вимкненими планами виконання, ось такі статистичні дані (усічені для стислості):

Table 'MyTable'. Scan count 1, logical reads 3...

SQL Server Execution Times:
  CPU time = 0 ms,  elapsed time = 0 ms.

А ось план запитів :

Горіхи


Не можете ви подолати імітацію обчисленого стовпця / відфільтрованого індексу з індексом на WHERE (50000000 BETWEEN RangeFrom AND RangeTo) INCLUDE (..)?
ypercubeᵀᴹ

3
@ yper-crazyhat-cubeᵀᴹ - так. CREATE INDEX IX1_redo ON dbo.MyTable (curtis_jackson) INCLUDE (RangeFrom, RangeTo) WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000працювали б. І запит SELECT * FROM MyTable WHERE RangeTo >= 50000000 AND RangeFrom <= 50000000;використовує його - тож не дуже потрібна бідна Кертіс
Мартін Сміт

3

Моє рішення грунтується на тому спостереженні , що інтервал має відому максимальну ширину W . Для вибіркових даних це один байт або 256 цілих чисел. Отже , для заданого значення параметра пошуку P ми знаємо найменше RangeFrom , що може бути в наборі результатів P - W . Додавання цього до присудка дає

declare @P int = 50000000;
declare @W int = 256;

select
    *
from MyTable
where @P between RangeFrom and RangeTo
and RangeFrom >= (@P - @W);

З огляду на оригінальну настройку та запит моя машина (64-бітна Windows 10, 4-ядерна гіпертокована i7, 2,8 ГГц, 16 ГБ оперативної пам’яті) повертає 13 рядів. Цей запит використовує паралельний пошук індексу (RangeFrom, RangeTo) індексу. Переглянутий запит також виконує паралельне пошуку індексу на тому ж індексі.

Вимірювання для оригінальних та переглянутих запитів є

                          Original  Revised
                          --------  -------
Stats IO Scan count              9        6
Stats IO logical reads       11547        6

Estimated number of rows   1643170  1216080
Number of rows read        5109666       29
QueryTimeStats CPU             344        2
QueryTimeStats Elapsed          53        0

Для оригінального запиту кількість прочитаних рядків дорівнює кількості рядків, менших або рівних @P. Оптимізатор запитів (QO) не має альтернативи, але прочитайте їх усі, оскільки він не може заздалегідь визначити, що, якщо ці рядки будуть задовольняти предикат. Індекс багато стовпців на (RangeFrom, RangeTo) не корисний для усунення рядків, які не відповідають RangeTo, оскільки немає кореляції між першим індексним ключем та другим, який можна застосувати. Наприклад, перший рядок може мати невеликий інтервал і бути виключеним, тоді як другий ряд має великий інтервал і повертається, або навпаки.

В одній невдалій спробі я намагався забезпечити цю впевненість через обмеження перевірки:

alter table MyTable with check
add constraint CK_MyTable_Interval
check
(
    RangeTo <= RangeFrom + 256
);

Це не мало значення.

Включивши мої зовнішні знання про розподіл даних у предикат, я можу змусити QO пропустити низькозначні рядки RangeFrom, які ніколи не можуть бути частиною набору результатів, і провести провідний стовпчик індексу до допустимих рядків. Це відображається в різних предикатах пошуку для кожного запиту.

У дзеркальному аргументі верхня межа RangeTo - P + W . Однак це не корисно, оскільки немає кореляції між RangeFrom та RangeTo, яка б дозволила останньому стовпчику індексу багато стовпців ліквідувати рядки. Отже, немає користі від додавання цього пункту до запиту.

Цей підхід отримує більшу частину користі від невеликого розміру інтервалу. Зі збільшенням можливого розміру інтервалу кількість низько оцінених рядків, які пропускаються, зменшується, хоча деякі з них все ще будуть пропущені. У обмежувальному випадку, з великим інтервалом, як діапазон даних, цей підхід не гірший за початковий запит (я визнаю, холодний комфорт).

Прошу вибачення за те, що в цій відповіді можуть існувати поодинокі помилки.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.