TSQL - подзапрос внутри Begin End - PullRequest
0 голосов
/ 10 декабря 2018

Рассмотрим следующий запрос:

begin 
;with 

t1 as (
    select top(10) x from tableX
),

t2 as (
    select * from t1 
),

t3 as (
    select * from t1 
)
-- --------------------------

select *     
from t2
join t3 on t3.x=t2.x

end
go

Мне было интересно, если t1 вызывается дважды, следовательно, tableX вызывается дважды (что означает, что t1 действует как таблица)?

или только один раз с его строками, сохраненными в t1 для всего запроса (как переменная в языке программирования)?

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

Ответы [ 3 ]

0 голосов
/ 10 декабря 2018

Если честно, ответа нет ... Единственный ответ: Гонки на лошадях (Эрик Липперт) .

То, как вы пишете свой запрос, не говорит вам, какдвигатель приведёт его в исполнение.Это зависит от многих, многих влияний ...

Вы говорите двигателю, что вы хотите получить, и двигатель решает , как получить это.

Это может даже отличаться между одинаковыми вызовами в зависимости от статистики, текущих запросов, существующих кэшированных результатов и т. Д.

Как подсказка, попробуйте следующее:

USE master;
GO
CREATE DATABASE testDB;
GO
USE testDB;
GO

- я создаютаблица физических тестов с 1.000.000 строк

CREATE TABLE testTbl(ID INT IDENTITY PRIMARY KEY, SomeValue VARCHAR(100));

WITH MioRows(Nr) AS (SELECT TOP 1000000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2 CROSS JOIN master..spt_values v3)
INSERT INTO testTbl(SomeValue)
SELECT CONCAT('Test',Nr)
FROM MioRows;

- Теперь мы можем начать тестировать это

GO
CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO
DECLARE @dt DATETIME2 = SYSUTCDATETIME();

- Ваш подход с CTE

;with t1 as (select * from testTbl)
     ,t2 as (select * from t1)
     ,t3 as (select * from t1)
select t2.ID AS t2_ID,t2.SomeValue AS t2_SomeValue,t3.ID AS t3_ID,t3.SomeValue AS t3_SomeValue  INTO target1   
from t2
join t3 on t3.ID=t2.ID;

SELECT 'Final CTE',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());
GO

CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO
DECLARE @dt DATETIME2 = SYSUTCDATETIME();

- Запись промежуточного результата в физическую таблицу

SELECT * INTO test1 FROM testTbl;
SELECT 'Write into test1',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());

select t2.ID AS t2_ID,t2.SomeValue AS t2_SomeValue,t3.ID AS t3_ID,t3.SomeValue AS t3_SomeValue  INTO target2   
from test1 t2
join test1 t3 on t3.ID=t2.ID

SELECT 'Final physical table',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());

GO
CHECKPOINT;
GO
DBCC DROPCLEANBUFFERS;
GO
DECLARE @dt DATETIME2 = SYSUTCDATETIME();

- То же, что и раньше, но с первичным ключом на промежуточной таблице

SELECT * INTO test2 FROM testTbl;
SELECT 'Write into test2',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());
ALTER TABLE test2 ADD PRIMARY KEY (ID);
SELECT 'Add PK',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());

select t2.ID AS t2_ID,t2.SomeValue AS t2_SomeValue,t3.ID AS t3_ID,t3.SomeValue AS t3_SomeValue  INTO target3  
from test2 t2
join test2 t3 on t3.ID=t2.ID

SELECT 'Final physical tabel with PK',DATEDIFF(MILLISECOND,@dt,SYSUTCDATETIME());

- Очистить (Осторожнос реальными данными !!!)

GO
USE master;
GO
--DROP DATABASE testDB;
GO

В моей системе

  • сначала занимает 674 мс,
  • второй 1,205 мс (297 для записи в * 1041)*) и
  • третьи 1,727 мс (285 для записи в test2 и ~ 650 мс для создания индекса.

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

Conclusio

Движок действительно умный ... Не пытайтесь быть умнее ...

Еслитаблица будет охватывать множество столбцов и гораздо больше данных в строке, весь тест может вернуть что-то еще ...

Если ваши CTE (подзапросы) содержат гораздо более сложные данные с объединениями, представлениями, функциями ии так, у двигателя могут возникнуть проблемы с поиском наилучшего подхода.

Если производительность имеет значение, вы можете мчаться на лошадях , чтобы проверить это. Один совет: я иногда использовал TABLE HINT довольно успешно: FORCE ORDER.Это выполнит соединения в порядке, указанном в запросе.

0 голосов
/ 11 декабря 2018

Вот простой пример для проверки теорий: во-первых, через временную таблицу, которая вызывает вопрос только один раз.

declare @r1 table (id int, v uniqueidentifier);

insert into @r1 
SELECT * FROM 
(
    select id=1, NewId() as 'v' union   
    select id=2, NewId()
) t
-- -----------

begin 
;with 

    t1 as (
        select * from @r1
    ),

    t2 as (
        select * from t1    
    ),

    t3 as (
        select * from t1    
    )

    -- ----------------

    select * from t2
    union all select * from t3

end
go

enter image description here


С другой стороны, если мы поместим вопрос в t1 вместо временной таблицы, он будет вызван дважды.

t1 as (
        select id=1, NewId() as 'v' union   
        select id=2, NewId()
    )

enter image description here

Следовательно, мой вывод - использовать временную таблицу, а не отвечать на cached результаты.Кроме того, я реализовал это в крупномасштабном запросе, который вызывал «материю» только дважды, а после перемещения во временную таблицу время выполнения уменьшилось наполовину !!

0 голосов
/ 10 декабря 2018

Просто создайте таблицу:

CREATE TABLE tableX
(
    x int PRIMARY KEY
);

INSERT INTO tableX
VALUES (1)
      ,(2)

Включите генерацию плана выполнения и выполните запрос.Вы получите что-то вроде этого:

enter image description here

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

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

...