2013-07-03 2 views
0

У меня есть 2 таблицы (не может изменить их)SQL Server родитель-ребенок присоединиться и низкая производительность запроса

Parent (id, date, amount) 
Child (parent_id, key, value) 

индексы

Parent.pk (id) 
Parent.idx1 (id, date) include (amount) 
Child.pk (parent_id, key) 
Child.idx1 (parent_id, key, value) 

и запрос

select sum(amount) 
from Parent as p 
left outer join Child as c1 on c1.parent_id = p.id and c1.key = 'X' 
left outer join Child as c2 on c2.parent_id = p.id and c2.key = 'Y' 
where p.date between '20120101' and '20120131' 
and c1.value = 'x1' 
and c2.value = 'y1' 

Проблема производительности.
Родитель имеет ~ 1 500 000 записей и ребенка ~ 6 000 000 записей

Берут 1

Этот запрос занимает ~ 3сек, который слишком много для моего сценария - он должен быть менее нескольких миллисекунд.

План выполнения показывает мне, что SQL Server выполняет индексное сканирование на Parent.idx1, а затем объединяет объединение с Child.idx1 кластеризованным индексом поиска - что не является оптимальным, потому что он сканирует все 1500000 записей, даже если я их фильтрую по дате.

Take 2

Когда я изменяю Parent.idx1 в

Parent.idx1 (date, id) include (amount) 

Sql сервер выбирает кластерный индекс сканирования на Parent.pk и чем снова слиянием с Child.idx1. Время выполнения ~ 6 с.

Возьмите 3

Когда я заставить его использовать Parent.idx1 (date, id) include (amount) затем сортирует результат до слияния и время выполнения еще хуже ~ 11s.

Возьмите 4

Пытался создать индексированного представления, но не может использовать его из-LEFT OUTER JOIN.

Есть ли способ сделать такой запрос - родитель-ребенок объединяется с фильтрами на обоих из них - быстрее?
Без дезадаптации.

Update 2013-07-04:
Для тех, ответив использовать INNER JOIN - Да, это намного быстрее, но я не могу его использовать.
Я показал здесь упрощенную версию того, что мне действительно нужно.
Мне нужно создать SQL-представление для таблиц MS Dynamics NAV «G/L Entry» (родительский) и «Ledger Entry Dimension» (Child), чтобы я мог прочитать его из этого приложения. Полный вид выглядит это прямо сейчас:

create view analysis 
as 
select 
    v.id as view_id 
    , p.date 
    , p.Amount 
    , c1.value as value1 
    , c2.value as value2 
    , c3.value as value3 
    , c4.value as value4 
from Parent as p 
    cross join analysis_view as v 
    left outer join Child as c1 on c1.parent_id = p.id and c1.key = v.key1 
    left outer join Child as c2 on c2.parent_id = p.id and c2.key = v.key2 
    left outer join Child as c3 on c3.parent_id = p.id and c3.key = v.key3 
    left outer join Child as c4 on c4.parent_id = p.id and c4.key = v.key4 

где analysis_view содержит 8 записей в данный момент и выглядит следующим образом: analysis_view (id, key1, key2, key3, key4)
и затем ПРИМЕНЕНИЕ может запросить его, как это

select sum(amount) 
from analysis 
where view_id = 1 and date between '20120101' and '20120131' 
and value1 = 'x1' 
and value2 = 'x2' 

или

select sum(amount) 
from analysis 
where view_id = 1 and date between '20120101' and '20120131' 
and value1 = 'x1' 
and value3 = 'z1' 

MS Dynamics NAV уже имеет нормализованную таблицу для нее и запросы от нее быстрые, но в нашем случае они огромны (~ 10 ГБ) и блокируют всю систему примерно на один час, когда кто-то создает новый анализ. Также NAV не знает, как создавать соединения, поэтому я должен определить его на стороне SQL Server.

+0

Являются ли «X» и «Y» единственными значениями 'key'? –

+0

Предполагаете ли вы рассчитывать сумму для записи дважды, если у нее есть как ребенок с ключом X, так и ключ Y? Или вы должны использовать сумму для суммирования, если у нее есть ребенок X или Y? Потому что вы делаете первое, но второе гораздо чаще. –

+1

«Менее нескольких миллисекунд» подразумевает, что все необходимые данные будут в кеше или любые операции ввода-вывода будут выполняться на SSD, возможно, в RAID-массиве, чтобы улучшить пропускную способность чтения. – HABO

ответ

0

Я выполняю несколько попыток, но я не нахожу ничего, что работает намного быстрее, чем просто исправление индексов.

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

Take 2: Создайте второе материализованное представление с родителем и child2 и используйте соединение между двумя материалами, снова - не намного быстрее.

Take 3: Используйте INTERSECT вместо JOIN, чтобы объединить два материализованных представления вместе - не намного быстрее.

Возьмите 4: Вспыхните DateTime в материализованных мнения в год и месяц колонн - не намного быстрее (на самом деле медленнее)

Большая проблема, кажется, у вас есть ограничения на столе ребенка в два раза, что исключает возможность выполнения материализованных представлений любым эффективным способом. Я могу написать материализованное представление, которое будет делать ежемесячные итоги поиска родителей с дочерним ключом «X» и значением «X1» очень быстро, путем предварительной их агрегации, но недостаточно информации, чтобы присоединиться к фильтру из сумм, которые не имеют отношения child2.

Это и я был ленив и старался выполнить тестирование производительности с 1/10-м количеством данных, которые у вас есть, и мои результаты были очень быстрыми (< 200 мс) независимо от того, что я сделал. Сейчас я строю полный набор тестовых данных, но, очевидно, я не знаю, что такое ваш дистрибутив. Это помогло бы узнать, сколько из 1500 000 записей имеют дети X, дети Y и дети X & Y. И если это фиксированный запрос или ключи/значения будут меняться во время выполнения.

Вот мои тестовые скрипты: Настройки:

CREATE TABLE Parent (id int NOT NULL CONSTRAINT parent_pk PRIMARY KEY, date datetime, amount decimal(10,2) NOT NULL) 
CREATE TABLE Child (parent_id int NOT NULL, [key] char(1) NOT NULL, value char(2) NOT NULL, CONSTRAINT child_pk PRIMARY KEY (parent_id,[key])) 
CREATE INDEX Parent_IDX ON Parent (id,date,amount) 
CREATE INDEX Child_IDX ON Child (parent_id,[key],value) 

DECLARE @RowCount INT 
DECLARE @Random INT 
DECLARE @Upper INT 
DECLARE @Lower INT 
DECLARE @InsertDate DATETIME 
DECLARE @keys INT 
DECLARE @key INT 

SET @Lower = 0 
SET @Upper = 500 
SET @RowCount = 0 
WHILE @RowCount < 15000 
BEGIN 

SELECT @Random = ROUND(((@Upper - @Lower -1) * RAND() + @Lower), 0) 
SET @InsertDate = DATEADD(dd, @Random, GETDATE()) 

INSERT INTO Parent(id,date,amount) 
VALUES (@RowCount , @InsertDate ,@Random) 

SET @keys=ROUND(RAND()*3+1,0) 
SET @key=0 
WHILE @key<@keys 
BEGIN 
INSERT INTO Child(parent_id,[key],value) 
VALUES (@RowCount,SUBSTRING('XYZ',@key+1,1),SUBSTRING('XYZ',@key+1,1)+'1') 
SET @[email protected]+1 
END 

SET @RowCount = @RowCount + 1 
END 

И мой сверхоперативное:

SELECT COUNT(*) ParentCount FROM Parent 
GO 
SELECT COUNT(*) ChildCount FROM Child 
GO 
CREATE INDEX Parent_IDX2 ON Parent(date,id) 
GO 
CREATE VIEW blah WITH SCHEMABINDING AS 
SELECT p.id,p.amount,DATEPART(YEAR,p.date) AS yy,DATEPART(Month,p.date) AS mm 
from dbo.Parent as p 
join dbo.Child as c1 on c1.parent_id = p.id and c1.[key] = 'X' and c1.value = 'x1' 
--join dbo.Child as c2 on c2.parent_id = p.id and c2.[key] = 'Y' and c2.value = 'y1' 
GO 
CREATE UNIQUE CLUSTERED INDEX blah_pk ON blah (id) 
CREATE INDEX blah_IDX ON blah (yy,mm,amount) 
GO 
CREATE VIEW blah2 WITH SCHEMABINDING AS 
SELECT p.id,p.amount,DATEPART(YEAR,p.date) AS yy,DATEPART(Month,p.date) AS mm 
from dbo.Parent as p 
join dbo.Child as c1 on c1.parent_id = p.id and c1.[key] = 'Y' and c1.value = 'y1' 

GO 
CREATE UNIQUE CLUSTERED INDEX blah2_pk ON blah2 (id) 
CREATE INDEX blah2_IDX ON blah2 (yy,mm,amount) 
GO 
select sum(amount) 
from Parent as p 
join Child as c1 on c1.parent_id = p.id and c1.[key] = 'X' and c1.value = 'x1' 
join Child as c2 on c2.parent_id = p.id and c2.[key] = 'Y' and c2.value = 'y1' 
where p.date between '20130801' and '20130831' 
GO 
select sum(amount) 
from blah p 
join Child as c2 on c2.parent_id = p.id and c2.[key] = 'Y' and c2.value = 'y1' 
where p.yy=2013 and p.mm=8 
GO 
SELECT sum(blah.amount) 
FROM blah 
JOIN blah2 ON blah.id=blah2.id AND blah.yy=blah2.yy AND blah.mm=blah2.yy and blah.amount=blah2.amount 
where blah.yy=2013 and blah.mm=8 

SELECT SUM(amount) 
FROM (
SELECT * 
FROM blah 
where blah.yy=2013 and blah.mm=8 
INTERSECT 
SELECT * 
FROM blah2 
where blah2.yy=2013 and blah2.mm=8 
) t1 
0

Есть несколько вещей, которые, влияющие на работу (хотя я не эксперт). Одна из этих вещей имеет индекс на Child, который имеет каждый столбец этой таблицы в качестве первичных столбцов индекса, что на самом деле не имеет смысла. Другое дело, что вы фильтруете свой запрос в соответствии со значениями таблиц c1 и c2, преобразуя свой запрос на INNER JOIN.Вы должны попытаться изменить его, чтобы использовать EXISTS вместо этого, что-то вроде этого:

select sum(amount) 
from Parent as p 
where p.date between '20120101' and '20120131' 
and exists(select 1 from Child 
      where parent_id = p.id and key = 'X' 
      and value = 'x1') 
and exists(select 1 from Child 
      where parent_id = p.id and key = 'Y' 
      and value = 'y1') 
+1

Семантика запроса OP требует наличия значений _both_ 'x1' и' y1'. Ваш запрос будет соответствовать, но не требует пар. – HABO

+0

@HABO вы правы, спасибо – Lamak

1

Измените свой LEFT JOIN в INNER JOIN. Предикат c1.value = 'x1' будет в любом случае отбрасывать внешние левые строки.

+0

см. Обновление, в котором я объяснил, почему я не могу его использовать – SeeR

Смежные вопросы