Как я могу улучшить скорость этого SQL-запроса?

У нас есть два устройства, собирающих данные с интервалом примерно в 30 секунд. Устройства расположены на двух разнесенных площадках. Абсолютное время каждой коллекции для каждого сайта может варьироваться +/- 30 секунд. Иногда сайт отключается по разным причинам. Данные с каждого устройства представляют разные виды измерений, например. температура с устройства1 и влажность с устройства2. Процесс записывает данные с устройства1 и устройства2 в отдельные таблицы в базе данных SQL Server 2012 Express, работающей на сервере, отдельном от каждого устройства.

Желательно представить данные с обоих устройств, коррелированные в записи, которые будут содержать столбцы со значением для сайта 1 для конкретной даты/времени в сочетании с данными для сайта 2, если таковые имеются. Затем пользовательские программы будут запрашивать наборы записей для указанного диапазона даты/времени. С этой целью я построил следующий SP:

ALTER PROCEDURE [db_datareader].[DataJoinDateRange] 
@DateFrom DateTime = '2014-05-15 15:10:24.000', 
@DateTo DateTime = '2014-06-15 15:10:24.000' 
AS
BEGIN
SET NOCOUNT ON;
WITH site1(id, date_time, dataval)
AS
(
    SELECT * 
    FROM site1_data 
    WHERE site1_data.date_time BETWEEN @DateFrom AND @DateTo
),
site2(id, date_time, datavaql)
AS
(
    SELECT *
    FROM site2_data
    WHERE site2_data.date_time BETWEEN @DateFrom AND @DateTo
)
SELECT * from site1 site1_res
INNER JOIN (select id, date_time, data_val) site2_res
on ABS(DATEDIFF("SECOND", site1_res.date_time, site_2_res.date_time)) < 30
END

Цель состоит в том, чтобы сначала выбрать записи в нужном диапазоне даты/времени, а затем соединить записи с сайта 1 с записями на сайте 2, которые находятся в пределах +/- 30 секунд. диапазон. Результирующий набор записей будет содержать данные с обоих устройств или пустые значения, если соответствующей записи не существует.

Вроде работает: выводятся записи нужной формы и соответствуют правильным записям в каждой таблице. Но исполнение очень медленное. Запрос в диапазоне дат в несколько недель занимает около 1 минуты 30 секунд. Site1 содержит около 5000 записей в этом диапазоне дат, а Site2 содержит только 1 запись. Запрос SELECT по диапазону дат только для каждой таблицы выполняется менее чем за секунду.

Раньше я никогда глубоко не вникал в SQL, но в нашей небольшой группе в настоящее время нет никого, кто мог бы выполнить эту задачу. Может ли кто-нибудь дать мне представление о правильном способе сделать это или, по крайней мере, как ускорить этот SP?


person Mozzis    schedule 03.02.2015    source источник
comment
Можете ли вы отредактировать поля date_time в таблице при загрузке данных, чтобы они всегда были: 00 или: 30, или добавить для этой цели новое поле (datetime или int) вместо вычисления datediff?   -  person James Z    schedule 04.02.2015
comment
Когда мне нужна скорость выполнения запроса, я всегда обращаюсь к индексам и смотрю, могу ли я сделать индекс только для текущей задачи. Вы также можете создавать индексы, которые пересекают таблицы.   -  person Reenactor Rob    schedule 04.02.2015
comment
В вашем подзапросе нет таблицы в запросе. Затем у вас есть предикаты, не поддерживающие SARG, потому что у вас есть столбцы в функции. И последнее, но не менее важное: у вас есть треугольное соединение. sqlservercentral.com/articles/T-SQL/61539   -  person Sean Lange    schedule 04.02.2015
comment
JamesZ, временные метки в данных по своей природе изменчивы, и я должен найти записи (если они есть) с site2, которые достаточно близки к временной метке в данной записи site1. Поэтому мне всегда нужно будет делать некоторые вычисления, чтобы определить, какие (если есть) записи с site2 подходят.   -  person Mozzis    schedule 04.02.2015
comment
Реконструктор Роб. Я создал индексы для поля date_time в обеих таблицах. Создание индекса, объединяющего обе таблицы, звучит интригующе, мне нужно выяснить, как это сделать, и попробовать.   -  person Mozzis    schedule 04.02.2015
comment
@SeanLange: Не могли бы вы прокомментировать ответ, который я разместил ниже? Я не вижу никакого способа обойти проблему невозможности анализа, поскольку я не знаю другого способа сопоставить две таблицы, кроме как с помощью вызова функции. Или было бы лучше создать временный локальный и использовать его?   -  person Mozzis    schedule 04.02.2015
comment
Не могли бы вы предоставить некоторые образцы данных?   -  person Felix Pamittan    schedule 04.02.2015


Ответы (2)


Вы можете попытаться улучшить свое решение, лучше используя индекс в столбце date_time.

ABS(S1 - S2) < 30

эквивалентно

ABS(S2 - S1) < 30

<=>

-30 < S2 - S1 < 30

<=>

S2 - S1 < 30
AND
S2 - S1 > -30

<=>

S2 < S1 + 30
AND
S2 > S1 - 30

Вам действительно не нужен первый CTE, хотя это не должно повредить. Но предложение WHERE внутри CROSS APPLY лучше писать так. Кроме того, вам следует использовать OUTER APPLY вместо CROSS APPLY, если вы хотите увидеть данные с сайта1, у которого нет соответствующих данных с сайта2. Теперь site2.date_time не находится внутри вызова функции, и оптимизатор может использовать индекс для этого столбца.

ALTER PROCEDURE [dbo].[SPJoinDateRange]
    @DateFrom DateTime = '2014-05-01 15:10:24.000', 
    @DateTo   DateTime = '2014-07-31 15:10:00.000'
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        site1_data.id AS id1
        ,site1_data.date_time AS date_time1
        ,site1_data.data_val1
        ,CA_site2.id2
        ,CA_site2.date_time2
        ,CA_site2.data_val2
    FROM
        site1_data
        OUTER APPLY
        (
            SELECT
                site2_data.id as id2
                ,site2_data.date_time as date_time2
                ,site2_data.data_val2
            FROM
                site2_data
            WHERE
                site2.date_time BETWEEN @DateFrom AND @DateTo
                AND site2.date_time < DATEADD(second, +30, site1_data.date_time)
                AND site2.date_time > DATEADD(second, -30, site1_data.date_time)
        ) AS CA_site2
    WHERE
        site1_data.date_time BETWEEN @DateFrom AND @DateTo
    ;

END

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

Если мы добавим столбец с именем date_time_rounded, который содержит исходную метку времени, округленную до 30 секунд, создадим для него индекс, то запрос будет выглядеть так:

ALTER PROCEDURE [dbo].[SPJoinDateRange]
    @DateFrom DateTime = '2014-05-01 15:10:24.000', 
    @DateTo   DateTime = '2014-07-31 15:10:00.000'
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        site1_data.id AS id1
        ,site1_data.date_time AS date_time1
        ,site1_data.data_val1
        ,site2_data.id AS id2
        ,site2_data.date_time AS date_time2
        ,site2_data.data_val2
    FROM
        site1_data
        LEFT JOIN site2_data ON site2_data.date_time_rounded = site1_data.date_time_rounded
    WHERE
        site1_data.date_time BETWEEN @DateFrom AND @DateTo
    ;

END

Чтобы округлить date_time до ближайших 30 секунд, вы можете использовать что-то вроде этого:

DATEADD(second, 30 * ROUND(DATEDIFF(second, '20010101', date_time)/30.0, 0), '20010101')

Он вычисляет количество секунд от 2001-01-01 до заданного date_time, делит их на 30, округляет результат до целого числа, умножает результат на 30, прибавляет это количество секунд к 2001-01-01.

Запустите это несколько раз, чтобы увидеть, как это работает:

SELECT 
GETDATE() as original, 
DATEADD(second, 30 * ROUND(DATEDIFF(second, '20010101', GETDATE())/30.0, 0), '20010101') AS rounded
person Vladimir Baranov    schedule 04.02.2015

Я нашел эту статью в другом месте, и она оказалась весьма полезной. В результате я изменил SP на следующее:

ALTER PROCEDURE [dbo].[SPJoinDateRange]
@DateFrom DateTime = '2014-05-01 15:10:24.000', 
@DateTo   DateTime = '2014-07-31 15:10:00.000'
AS
BEGIN
SET NOCOUNT ON;
WITH site1(id, date_time, data_val1)
AS
(
    SELECT * 
    FROM site1_data 
    WHERE site1_data.date_time BETWEEN @DateFrom AND @DateTo
)
SELECT * FROM site1
CROSS APPLY 
(
    SELECT id as id1, date_time as date_time1, data_val2
    FROM site2_data AS site2
    WHERE site2.date_time BETWEEN @DateFrom AND @DateTo 
    AND 
    ABS(DATEDIFF("SECOND", site1.date_time, site2.date_time)) < 30
) 
AS result
END

Время результата для этого запроса составляет 6 секунд. (по сравнению с 90 секундами для предыдущей версии.) Это все еще может быть намного медленнее, чем возможно; моей следующей задачей в идеале было бы понять, почему этот подход работает быстрее. Лаконичный ответ (и ссылка) от Шона Ланге, несомненно, дает некоторые подсказки. Конечно, мне придется отложить это и перейти к следующей задаче в нашей первоначальной реализации.

Спасибо всем, кто так быстро откликнулся на мой вопрос.

person Mozzis    schedule 03.02.2015
comment
Похоже, производительность значительно улучшилась. Поздравляю! Я подозреваю, что это можно сделать быстрее, чем за 6 секунд, но, не зная структуры таблиц и индексов, трудно сказать, что было бы лучше. Одно предложение: если вы собираетесь использовать cte site1, вы должны явно указать столбцы в выборе вместо использования *. Если структура вашей таблицы изменится, ваш запрос будет нарушен. - person Sean Lange; 04.02.2015