Фильтрация данных по порядку строк

8

У меня есть таблица данных SQL со следующей структурой:

CREATE TABLE Data(
    Id uniqueidentifier NOT NULL,
    Date datetime NOT NULL,
    Value decimal(20, 10) NULL,
    RV timestamp NOT NULL,
 CONSTRAINT PK_Data PRIMARY KEY CLUSTERED (Id, Date)
)

Число различных идентификаторов варьируется от 3000 до 50000.
Размер таблицы варьируется до более миллиарда строк.
Один идентификатор может занимать от нескольких строк до 5% таблицы.

Единственный наиболее выполненный запрос в этой таблице:

SELECT Id, Date, Value, RV
FROM Data
WHERE Id = @Id
AND Date Between @StartDate AND @StopDate

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

Я написал эту процедуру:

CREATE TYPE guid_list_tbltype AS TABLE (Id uniqueidentifier not null primary key)
CREATE PROCEDURE GetData
    @Ids guid_list_tbltype READONLY,
    @Cursor rowversion,
    @MaxRows int
AS
BEGIN
    SELECT A.* 
    FROM (
        SELECT 
            Data.Id,
            Date,
            Value,
            RV,
            ROW_NUMBER() OVER (ORDER BY RV) AS RN
        FROM Data
             inner join (SELECT Id FROM @Ids) Ids ON Ids.Id = Data.Id
        WHERE RV > @Cursor
    ) A 
    WHERE RN <= @MaxRows
END

Где @MaxRowsбудет варьироваться от 500 000 до 2 000 000, в зависимости от того, как клиент будет запрашивать свои данные.


Я пробовал разные подходы:

  1. Индексирование по (Id, RV):
    CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, RV) INCLUDE(Date, Value);

Используя индекс, запрос ищет строки, где RV = @Cursorдля каждого Idиз них @Ids, читает следующие строки, затем объединяет результат и сортирует.
Эффективность зависит от относительного положения @Cursorстоимости.
Если он близок к концу данных (по заказу RV), запрос выполняется мгновенно, а если нет, то запрос может занять до нескольких минут (никогда не позволяйте ему выполняться до конца).

проблема с этим подходом заключается в том, что @Cursorлибо в конце данных, и сортировка не является болезненной (даже не требуется, если запрос возвращает меньше строк @MaxRows), либо она находится позади, и запрос должен сортировать @MaxRows * LEN(@Ids)строки.

  1. Индексирование по RV:
    CREATE NONCLUSTERED INDEX IDX_RV ON Data(RV) INCLUDE(Id, Date, Value);

Используя индекс, запрос ищет строку, где RV = @Cursorзатем читает каждую строку, отбрасывая незапрошенные идентификаторы, пока не достигнет @MaxRows.
Эффективность зависит от% запрашиваемых идентификаторов ( LEN(@Ids) / COUNT(DISTINCT Id)) и их распределения.
Больше запрашиваемого Id% означает меньше отброшенных строк, что означает более эффективное чтение, меньшее количество запрашиваемых Id% означает больше отброшенных строк, что означает больше чтений для того же количества результирующих строк.

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

  1. Использование отфильтрованного индекса или индексированных представлений
    CREATE NONCLUSTERED INDEX IDX_RVClient1 ON Data(Id, RV) INCLUDE(Date, Value)
    WHERE Id IN (/* list of Ids for specific client*/);

Или

    CREATE VIEW vDataClient1 WITH SCHEMABINDING
    AS
    SELECT
        Id,
        Date,
        Value,
        RV
    FROM dbo.Data
    WHERE Id IN (/* list of Ids for specific client*/)
    CREATE UNIQUE CLUSTERED INDEX IDX_IDRV ON vDataClient1(Id, Rv);

Этот метод обеспечивает совершенно эффективные планы индексирования и выполнения запросов, но имеет недостатки: 1. На практике мне придется реализовать динамический SQL для создания индексов или представлений и изменить процедуру запроса для использования правильного индекса или представления. 2. Мне придется поддерживать один индекс или представление существующим клиентом, включая хранилище. 3. Каждый раз, когда клиенту нужно будет изменить свой список запрашиваемых идентификаторов, мне нужно будет удалить индекс или просмотреть и воссоздать его.


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

Paciv
источник
Кросспост с stackoverflow.com/questions/11586004/… . Я удалил версию Oracle на данный момент, потому что обнаружил, что ORA_ROWSCN не индексируется (и вряд ли с помощью индексированных материализованных представлений).
Paciv
Как вписывается поле даты? Можно ли в таблице обновить строку с определенным идентификатором и датой? И если да, обновляется ли дата (например, дополнительная временная метка?)
8kb
Похоже, для попытки GetData (), порядок должен включать Id (порядок по RV, Id). Можете ли вы прокомментировать использование индекса (Rv, Id)? Также при использовании «>» max rowversion из предыдущего вызова кажется, что он пропустит записи между кусками, если строки имеют одинаковую версию строки (разве это не возможно?).
Crokusek
@ 8kb: операторы обновления, выполняемые в таблице, изменяют только Valueстолбец. @crokusek: Заказ по RV, ID вместо RV только не увеличит рабочую нагрузку сортировки без какой-либо выгоды, я не понимаю причины вашего комментария. Из того, что я прочитал, RV должен быть уникальным, если не вставлять данные конкретно в этот столбец, чего нет в приложении.
Paciv
Может ли клиент принимать результаты в порядке (Id, Rv) и предоставлять аргумент LastId в дополнение к аргументу LastRowVersion для исключения сортировки RV по идентификаторам? Мои предыдущие комментарии были основаны на предположении, что у Р.В. были дубликаты. Отфильтрованный индекс на клиента выглядел интересно.
Crokusek

Ответы:

5

Одним из решений для клиентского приложения является запоминание максимального значения для rowversionкаждого идентификатора. Пользовательский тип таблицы изменится на:

CREATE TYPE
    dbo.guid_list_tbltype
AS TABLE 
    (
    Id      uniqueidentifier PRIMARY KEY, 
    LastRV  rowversion NOT NULL
    );

Затем запрос в процедуре можно переписать для использования APPLYшаблона (см. Мои статьи SQLServerCentral, часть 1 и часть 2 - требуется бесплатный вход в систему). Ключ к хорошей производительности здесь ORDER BY- это позволяет избежать неупорядоченной предварительной выборки при объединении вложенных циклов. Это RECOMPILEнеобходимо для того, чтобы оптимизатор мог видеть мощность табличной переменной во время компиляции (возможно, в результате получился желательный параллельный план).

ALTER PROCEDURE dbo.GetData

    @IDs        guid_list_tbltype READONLY,
    @MaxRows    bigint

AS
BEGIN

    SELECT TOP (@MaxRows)
        d.Id,
        d.[Date],
        d.Value,
        d.RV
    FROM @Ids AS i
    CROSS APPLY
    (
        SELECT
            d.*
        FROM dbo.Data AS d
        WHERE
            d.Id = i.Id
            AND d.RV > i.LastRV
    ) AS d
    ORDER BY
        i.Id,
        d.RV
    OPTION (RECOMPILE);

END;

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

план запроса

Пол Уайт 9
источник
Правильно, одно из решений по изменению дизайна заключается в том, чтобы клиент запоминал MAX(RV)per Id (или систему подписки, где внутреннее приложение запоминает все пары Id / RV), и я использую этот шаблон для другого клиента. Другое решение состояло в том, чтобы заставить клиента всегда получать все идентификаторы (что делает проблему индексации тривиальной). Это все еще не покрывает особую потребность вопроса: добавочный поиск подмножества идентификаторов только с одним глобальным счетчиком, предоставленным клиентом.
Paciv
2

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

CREATE NONCLUSTERED INDEX IDX_IDRV ON Data(Id, VersionNumber) INCLUDE(Date, Value);

Конечно, нам нужно убедиться, что VersionNumber начинается с единицы и не имеет пробелов. Это легко сделать с ограничениями.

Аляска
источник
Вы имеете в виду глобальный или локальный идентификатор VersionNumber? В любом случае, я не вижу, как это поможет с вопросом, не могли бы вы уточнить?
Paciv
0

Что бы я сделал:

В этом случае ваш PK должен быть полем идентификации «Суррогатный ключ», который автоматически увеличивается.
Поскольку вы уже в миллиардах, было бы лучше пойти с BigInt.
Давайте назовем это DataID .
Это будет:

  • Добавьте 8 байтов к каждой записи в вашем кластерном индексе.
  • Сохраните 16 байт для каждой записи в каждом некластеризованном индексе.
  • У вас был «естественный ключ»: UniqueIdentifyer (16 байт) с датой DateTime (8 байт).
  • Это 24 байта в каждой записи индекса, чтобы вернуться к кластерному индексу!
  • Вот почему у нас есть суррогатные ключи как меньшие целые числа.


Настройте ваш новый BigInt PK ( DataID ) для использования Clustered-Index:
Это будет:

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


Создайте некластеризованный индекс вокруг (Date, Id).
Это будет:

  • Ускорьте ваши наиболее часто используемые запросы.
  • Вы можете добавить «Значение», но это увеличит размер вашего индекса, что сделает его медленнее.
  • Я бы посоветовал попробовать его внутри и за пределами индекса, чтобы увидеть, насколько велика разница в производительности.
  • Я бы рекомендовал не использовать «Включить», если вы добавите его.
  • Просто добавьте это (Date, Id, Value) - но только если ваше тестирование показывает, что это повышает производительность.


Создайте некластеризованный индекс на (RV, ID).
Это будет:

  • Всегда держите ваши индексы как можно меньше.
  • Если вы не заметите сумасшедшего огромного прироста производительности с указанием даты и значения в своих индексах, я бы посоветовал вам не использовать их для экономии места на диске. Попробуйте сначала без них.
  • Если вы добавляете «Дата» или «Значение», не используйте «Включить», вместо этого добавьте их к порядку индекса.
  • Благодаря инкременту DataID, добавляемому к новым вставкам в кластеризованный ПК, ваши последние RV обычно появляются ближе к концу (если вы не обновляете ряд данных из прошлого все время).
MikeTeeVee
источник