Ищите более производительный способ получения двух верхних строк группы в SQL - PullRequest
0 голосов
/ 05 марта 2019

Я ищу эффективный способ получения двух верхних строк для каждой группы данных в SQL.У меня очень большая таблица данных (около 10 миллиардов строк).Каждая строка данных описывается четырьмя измерениями (которые составляют первичный ключ), а таблица разбивается на одно из измерений (последний столбец первичного ключа).

-- Medium table (2 to 3 million rows)
CREATE TABLE [smallDatabase].[dbo].[dimTableA] (
    [colA] [int] NOT NULL PRIMARY KEY
    ,[valueA] [int]
);

-- Small table (<1000 rows)
CREATE TABLE [smallDatabase].[dbo].[dimTableB] (
    [colB] [int] NOT NULL PRIMARY KEY
    ,[valueB] [int]
);

-- Small table (<10000 rows)
CREATE TABLE [smallDatabase].[dbo].[dimTableC] (
    [colC] [int] NOT NULL PRIMARY KEY
    ,[valueC] [int]
);

-- Small table (100 to 200 rows)
CREATE TABLE [smallDatabase].[dbo].[dimTableD] (
    [colD] [int] NOT NULL PRIMARY KEY
    ,[grouperD] [int] NOT NULL
    ,[dateD] [date]
);

CREATE PARTITION FUNCTION [pfColD](int) AS RANGE RIGHT FOR VALUES (1, 2, 3, ..., n);
CREATE PARTITION SCHEME [psColD] AS PARTITION [pfColD] TO ([PRIMARY], [PRIMARY], [PRIMARY], ..., [PRIMARY]);

-- Large table (~10 billion rows)
CREATE TABLE [bigDatabase].[dbo].[factBigTable] (
    [colA] [int] NOT NULL
    ,[colB] [int] NOT NULL
    ,[colC] [int] NOT NULL
    ,[colD] [int] NOT NULL
    ,[datum] [float] NULL
    ,PRIMARY KEY (
        [colA] ASC
        ,[colB] ASC
        ,[colC] ASC
        ,[colD] ASC
    )
) ON psColD([colD]);

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

CREATE TABLE #filter (
    [colA] [int] NOT NULL
    ,[colB] [int] NOT NULL
    ,PRIMARY KEY (
        [colA] ASC
        ,[colB] ASC
    )
);

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

-- Get the most recent two data points for each group of data
SELECT *
FROM (
    SELECT big.*
        ,dimD.[grouperD]
        ,ROW_NUMBER() OVER (
            PARTITION BY dimD.[grouperD], big.[colA], big.[colB], big.[colC]
            ORDER BY dimD.[dateD] DESC
        ) AS rowNumber
    FROM [bigDatabase].[dbo].[factBigTable] AS big
    INNER JOIN [smallDatabase].[dbo].[dimTableD] AS dimD
        ON big.[colD] = dimD.[colD]
    INNER JOIN #filter
        ON big.[colA] = #filter.[colA]
            AND big.[colB] = #filter.[colB]
) AS bigDataRanked
WHERE rowNumber <= 2;

Это действительно дает мне точные данные, которые я ищу;тем не менее, он очень медленный!

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

Один алгоритм, который я попробовал, выглядел великолепно на бумаге, но в итоге работал очень медленно из-за редкой природы данных.Идея была:

  1. Кэшировать список каждой группы.То есть, [grouperD], [colA], [colB] и [colC].Следите за строками, найденными для каждой группы.
  2. Курсор над [colD], упорядоченный по [dateD].Остановитесь, когда в каждой группе будет найдено 2 строки.
  3. Выберите строки из [factBigTable], которые соответствуют группам, в которых количество найденных строк меньше 2. Кэшируйте результаты.
  4. Для кэшированных результатов увеличивайте количество найденных строк.
  5. Перемещение кэшированных результатов в промежуточную таблицу для последующего использования.
  6. Переход к следующему [colD] в цикле.

Каждый цикл выполняется относительно быстро, так как SQLумеет использовать PK, ищет по большинству запросов.Тем не менее, в некоторых из моих групп было очень низкое максимальное значение [colD], поэтому цикл должен был повторяться много раз.

Самое быстрое решение, которое я нашел до сих пор, выглядит ужасно на бумаге, но в итоге дает лучшие результаты.Тем не мение;он все еще медленнее, чем мне хотелось бы, и масштабируется очень плохо.

  1. Для подмножества данных, которое нам небезразлично (т. е. присоединение к фильтру), кэшируйте все первичные ключи для каждой группы.
  2. Выберите и кэшируйте максимальное значение [colD] для каждой группы.
  3. Удалите максимальное значение из списка PK по группам.
  4. Выберите и кэшируйте максимальное значение [colD] для каждой группы.группа, снова.Чтобы получить секунду для max [colD].
  5. Используйте ключи max и second для max, чтобы найти все нужные нам строки.

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

1 Ответ

0 голосов
/ 06 марта 2019

не могу комментировать, недостаточно большой, но любопытно, о какой версии sql server мы говорим?

Я сомневаюсь, что это будет быстрее (особенно если мы говорим о миллиардах строк до rowNumber <= 2), но я всегда предпочел бы разгрузить операцию row_number () в меньшее подмножество, если это возможно. </p>

;with bigDataRanked as (
SELECT big.*
        ,dimD.[grouperD]
        ,dimD.[dateD]
    FROM [bigDatabase].[dbo].[factBigTable] AS big
    INNER JOIN [smallDatabase].[dbo].[dimTableD] AS dimD
        ON big.[colD] = dimD.[colD]
    INNER JOIN #filter
        ON big.[colA] = #filter.[colA]
            AND big.[colB] = #filter.[colB]
) 
select bdr.*, ROW_NUMBER() OVER (
            PARTITION BY bdr.[grouperD], bdr.[colA], bdr.[colB], bdr.[colC]
            ORDER BY bdr.[dateD] DESC) rowNumber
from bigDataRanked bdr -- will have an additional column dateD returned from dimTableD (from bdr.*)
where rowNumber <= 2;
...