Странная функция SQL Server 2005 странная производительность - PullRequest
2 голосов
/ 16 ноября 2010

У меня огромная разница во времени выполнения между 1-минутным и тем же запросом в табличной функции.

Но самое утомительное - это запуск UDF с другим (действительным) аргументом company_id. дает мне результат через ~ 40 секунд, и как только я изменяю этот company_id на 12 (снова действует), он никогда не останавливается. Планы выполнения этих двух запросов абсолютно не совпадают и, конечно, длинный является наиболее сложным. НО план выполнения между пакетной версией и версией UDF одинаков И И пакетная версия быстрая ...!

Если я выполняю следующий запрос «вручную», время выполнения составляет 1 мин 36 с 306 строками:

 SELECT
  dbo.date_only(Call.date) AS date,
  count(DISTINCT customer_id) AS new_customers
 FROM
  Call
 LEFT OUTER JOIN
  dbo.company_new_customers(12, 2009, 2009) new_customers
   ON dbo.date_only(new_customers.date) = dbo.date_only(Call.date)
 WHERE
  company_id = 12
  AND year(Call.date) >= 2009
  AND year(Call.date) <= 2009
 GROUP BY
  dbo.date_only(Call.date)

Я сохранил этот же запрос в функции и запустил его так:

SELECT * FROM company_new_customers_count(12, 2009, 2009)

13 минут пока он работает ... И я уверен, что он никогда не даст мне никакого результата.

Вчера у меня было такое же поведение, похожее на бесконечный цикл, в течение более 4 часов (поэтому я остановил его).

Вот определение функции:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION company_new_customers_count 
( 
 @company_id int, 
 @start_year int,
 @end_year int
)
RETURNS TABLE 
AS
RETURN 
(
 SELECT
  dbo.date_only(Call.date) AS date,
  count(DISTINCT customer_id) AS new_customers
 FROM
  Call
 LEFT OUTER JOIN
  dbo.company_new_customers(@company_id, @start_year, @end_year) new_customers
   ON dbo.date_only(new_customers.date) = dbo.date_only(Call.date)
 WHERE
  company_id = @company_id
  AND year(Call.date) >= @start_year
  AND year(Call.date) <= @end_year
 GROUP BY
  dbo.date_only(Call.date)
)
GO

Я был бы очень рад понять, что происходит.

Спасибо

Дополнительно:

Определение company_new_customers:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Description: Create the list of new customers of @company_id
--          in the given period.
-- =============================================
CREATE FUNCTION company_new_customers 
(   
    @company_id int, 
    @start_year int,
    @end_year   int
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT
        customer_id,
        date
    FROM
    (   -- select apparition dates of cutomers before @end_year
        SELECT
            min(date)       AS date,
            customer_id
        FROM
            Call
        JOIN
            Call_Customer ON Call_Customer.call_id = Call.call_id
        WHERE
            company_id = @company_id
            AND year(date) <= @end_year
        GROUP BY
            customer_id
    ) new_customers
    WHERE
        year(date) >= @start_year -- select apparition dates of cutomers after @start_year
)
GO

Определение даты только:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Julio Guerra
-- Create date: 14/10/2010
-- Description: Return only the date part of a datetime value
-- Example:         date_only('2010-10-25 13:00:12') returns 2010-10-25
-- =============================================
CREATE FUNCTION date_only
(
    @datetime datetime
)
RETURNS datetime
AS
BEGIN
    RETURN dateadd(dd, 0, datediff(dd, 0, @datetime))
END
GO

План выполнения SELECT * ИЗ company_new_customers_count (8, 2009, 2009) alt text

План выполнения SELECT * ОТ company_new_customers_count (12, 2009, 2009) alt text

Ответы [ 4 ]

2 голосов
/ 16 ноября 2010

Краткий план использует HashJoin и сканирование кластерного индекса на PK_CALL. Длинный план использует NestedLoops и повторный поиск в UK_Pair_.... Скорее всего, оценки количества элементов для «12, 2009, 2009» исключают HashJoin из-за недостатка системной памяти, поэтому у вас получается худший план (несмотря на поиск вместо сканирования). Возможно, company_id 12 имеет гораздо больше клиентов, чем company_id 8.

Невозможно дать решение без точной информации обо всех задействованных факторах (точная используемая схема, включая каждый индекс, а также точная статистика и количество элементов каждой задействованной таблицы). Простой способ добиться этого - использовать руководство по плану, см. Разработка и внедрение руководств по плану .

2 голосов
/ 16 ноября 2010

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

CREATE INDEX IX_call_company_date ON call (company_id, date)

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

1 голос
/ 16 ноября 2010

Здесь есть несколько частей ответа.В первой части я попытаюсь ответить на вопрос (вы не спрашивали) о том, почему ни один из запросов не является особенно быстрым.Это имеет отношение к вопросу, который вы на самом деле задали, так что терпите меня.

Ваши критерии даты обычно не SARGable , как вы их указали - например, в вашей функции company_new_customers.Это означает, что сервер не может использовать свою статистику, чтобы выяснить, насколько избирательными являются ваши критерии. Это означает, что ваши планы запросов будут очень и очень чувствительны к тому, насколько избирательным является ваш критерий customer_id , независимо от того, сколько строк дат у вас на клиента.

Использование запроса диапазона с индексом по дате и call_id в таблице вызовов должно значительно повысить производительность во всех случаях и снизить чувствительность вашего запроса к селективности customer_id.Предполагая, что дата указана в вашей таблице вызовов, я переписал бы ваш внутренний UDF примерно так и настроил бы входные параметры, чтобы вместо них использовать даты.Это также сделает ваш UDF более универсальным:

CREATE FUNCTION company_new_customers 
(   
    @company_id INT, 
    @start_date DATETIME,
    @end_date   DATETIME
)
RETURNS TABLE 
AS
RETURN 
(    
        SELECT
            MIN(c.[date]) AS [date],
            c.customer_id
        FROM dbo.[Call] c
        JOIN dbo.[Call_Customer] cc
            ON cc.call_id = c.call_id
        WHERE c.company_id = @company_id
        AND   c.[date]    <= @end_date
        AND   NOT EXISTS (
                SELECT *
                FROM  dbo.[Call] c1
                WHERE c1.customer_id = c.customer_id
                AND   c1.[date] <= @start_date              
        )    
        GROUP BY
            c.customer_id          
)
GO

То же самое относится и к другому представлению.Используя функции year () и date_only (), вы создаете любую статистику или индексы, которые у вас есть на ваших датах, но практически бесполезны (хотя оптимизатор может использовать их для ограничения объема сканируемых данных, но это большая дискуссия).*

Теперь тогда - почему ваш UDF работает вечно?Поскольку он вызывает другой UDF, и вы используете date_only () в качестве аргумента соединения, он почти ничего не может «знать» о том, чего ожидать в подзапросе UDF, поэтому он выбрал цикл объединения.Вероятно, он выбирает этот план, потому что он подходит для некоторых значений customer_id.Вероятно, вы выполнили запрос к одному из этих выборочных customer_ids вскоре после создания UDF, и план для этого запроса был кэширован - даже если он не подходит для других значений customer_id.

Почемухранимые процедуры не вечны?Поскольку при первом запуске хранимый процесс генерирует план на основе первых критериев, которые вы ему дали.Возможно, в первый раз, когда вы запустили SP, вы использовали неселективный идентификатор клиента, и сохраненный процесс выбрал хеш-соединение.То же самое и со специальным запросом.Оптимизатор «замечает», что вы передали ему неселективный customer_id, и решает создать хеш-соединение для вас.

В любом случае, если вы не контролируете проблему date-SARGability, выВы обнаружите, что все ваши запросы, подобные этому, будут очень чувствительны к вашему входу customer_id, и в зависимости от ваших шаблонов использования они могут взорваться вам в плане производительности - UDF или нет.

Надеюсь, это поможет!

0 голосов
/ 16 ноября 2010

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

К сожалению, у нас не было времени для более глубокой диагностики этого странного поведения, и мы переместили проект на избегание функций табличных значений в 2005 году.Server 2005.

...