SQL объединяется с диапазоном значений (диапазоны int, диапазоны дат и т. Д.) - PullRequest
4 голосов
/ 30 июня 2009

У меня есть две таблицы, первая - большая таблица (миллионы строк), наиболее интересным столбцом является целое число, которое я просто назову «ключом». Я полагаю, что это решение было бы идентичным и для диапазонов даты и времени.

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

key_lower_bound: int key_upper_bound: int интересно_значение1: плавать интересное_значение2: int интересно_значение3: varchar (50) ...

Я хочу посмотреть все значения в первой таблице и "соединить" их со второй таблицей, основываясь на том, попадает ли ключ в первой таблице в интервал [key_lower_bound, key_upper_bound).

Это математически похоже на разреженный внутренний продукт или произведение разреженных точек, но это немного странно, поскольку эти диапазоны включены во вторую таблицу. Тем не менее, если бы я написал это в коде, это был бы алгоритм O (| первая таблица | + | вторая таблица |). Я бы держал указатель в обоих (отсортированных) списках и просматривал каждый из них, чтобы определить, принадлежал ли каждый ключ в первой таблице диапазону второй таблицы. Хитрость в том, что я не перебираю второй список каждый раз, когда проверяю ключ в первой таблице, потому что оба списка отсортированы.

Когда я создаю наиболее очевидный SQL-запрос (включающий проверку того, что ключ> key_lower_bound и

В этом наивном запросе происходит какое-то квадратичное поведение, потому что я думаю, что механизм запросов выполняет каждое сравнение с каждой строкой во второй таблице, хотя в действительности, если вторая таблица сортируется по key_lower_bounds, это не должно быть необходимым Поэтому я получаю поведение типа O (| первая таблица | x | вторая таблица |) вместо желаемого поведения O (| первая таблица | + | вторая таблица |).

Возможно ли получить линейный SQL-запрос для этого?

Ответы [ 4 ]

6 голосов
/ 30 июня 2009

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

CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY)
GO
DECLARE @i INT;
SET @i = 1;
INSERT INTO dbo.Numbers(n) SELECT 1;
WHILE @i<1024000 BEGIN
  INSERT INTO dbo.Numbers(n)
    SELECT n + @i FROM dbo.Numbers;
  SET @i = @i * 2;
END;
GO

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

CREATE TABLE dbo.Commercials(
  StartedAt DATETIME NOT NULL 
    CONSTRAINT PK_Commercials PRIMARY KEY,
  EndedAt DATETIME NOT NULL,
  CommercialName VARCHAR(30) NOT NULL);
GO
INSERT INTO dbo.Commercials(StartedAt, EndedAt, CommercialName)
SELECT DATEADD(minute, n - 1, '20080101')
    ,DATEADD(minute, n, '20080101')
    ,'Show #'+CAST(n AS VARCHAR(6))
  FROM dbo.Numbers
  WHERE n<=24*365*60;
GO
CREATE TABLE dbo.Calls(CallID INT 
  CONSTRAINT PK_Calls NOT NULL PRIMARY KEY,
  AirTime DATETIME NOT NULL,
  SomeInfo CHAR(300));
GO
INSERT INTO dbo.Calls(CallID,
  AirTime,
  SomeInfo)
SELECT n 
    ,DATEADD(minute, n - 1, '20080101')
    ,'Call during Commercial #'+CAST(n AS VARCHAR(6))
  FROM dbo.Numbers
  WHERE n<=24*365*60;
GO
CREATE UNIQUE INDEX Calls_AirTime
  ON dbo.Calls(AirTime) INCLUDE(SomeInfo);
GO

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

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Commercials s JOIN dbo.Calls c 
  ON c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 15 ms, elapsed time = 30 ms.

(1 row(s) affected)
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 2, logical reads 3338264, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Commercials'. Scan count 2, logical reads 7166, 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.

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

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

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

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Commercials s JOIN dbo.Calls c 
  ON c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
AND s.StartedAt BETWEEN '20080630 23:45' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 15 ms, elapsed time = 15 ms.

(1 row(s) affected)
Table 'Worktable'. Scan count 1, logical reads 753, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Commercials'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

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

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

SELECT COUNT(*) FROM(
SELECT s.StartedAt, s.EndedAt, c.AirTime
FROM dbo.Calls c CROSS APPLY(
  SELECT TOP 1 s.StartedAt, s.EndedAt FROM dbo.Commercials s 
  WHERE c.AirTime >= s.StartedAt AND c.AirTime < s.EndedAt
  ORDER BY s.StartedAt DESC) AS s
WHERE c.AirTime BETWEEN '20080701' AND '20080701 03:00'
) AS t;

SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 7 ms.

(1 row(s) affected)
Table 'Commercials'. Scan count 181, logical reads 1327, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Calls'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:
   CPU time = 31 ms,  elapsed time = 31 ms.
1 голос
/ 30 июня 2009

Для первой таблицы я бы поставил кластерный индекс на "ключ". Для второй таблицы я бы поставил кластерный индекс на «key_lower_bound». Тогда я бы попробовал:

select *
from FirstTable f
inner join SecondTable s 
    on f.key between s.key_lower_bound and s.key_upper_bound

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

0 голосов
/ 30 июня 2009

Для выполнения описанного вами линейного алгоритма потребуются 2 вещи, которых нет в базе данных:

  1. Способность понимать, что каждая строка в вашей маленькой таблице содержит отдельное (непересекающееся) отображение множества LargeTable.Key в один SmallTable.Range, и
  2. Таблицы должны храниться в виде связанных списков или массивов (которыми они не являются).

Я полагаю, что наиболее близким к описанному вами поведению будет объединение слиянием :

выберите t1.key от LargeTable T1 внутреннее слияние, объединение smallTable t2 для t1.key> = t2.key_lower_bound и t1.key

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

В качестве вторичной идеи производительности вы должны попытаться определить одно значение, представляющее диапазон, и использовать его в качестве первичного ключа smallTable, на которое можно ссылаться из largeTable в качестве внешнего ключа. Это более эффективно, чем составной ключ (по сути, это то, что представляют ваши столбцы lower_bound и upper_bound). Возможно хешированное значение, например PK = lower_bound & upper_bound << определенное количество бит </p>


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

0 голосов
/ 30 июня 2009

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

Аналогичная проблема, решаемая денормализацией:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/03/08/storing-intervals-of-time-with-no-overlaps.aspx

Дайте мне знать, если вам нужен полный DDL, написать очень легко.

...