W przypadku używania w Oracle Analytics wielowymiarowej bazy danych jako źródła danych mogą wystąpić problemy z wydajnością skutkujące generowaniem nieoptymalnych zapytań z wyrażeniami wielowymiarowymi (MDX).
Modyfikując projekt, można poprawić zapytania MDX generowane przez Oracle Analytics. Może to mieć ogromny wpływ nie tylko na wydajność raportów, ale również na ilość zasobów używanych w bazie danych. Sposób korzystania z obsługiwanych lub nieobsługiwanych funkcji znacząco wpływa na generowane zapytania MDX, a tym samym na wydajność.
Ponieważ każdy przypadek użycia jest niepowtarzalny, zespół programistów powinien przejrzeć opcje, przeanalizować dzienniki zapytań Oracle Analytics i wybrać najlepsze rozwiązanie dla danego przypadku.
Ten temat nie obejmuje problemów z wydajnością wynikających z używanej infrastruktury, czyli np. sieci, przeglądarek lub sposobów prezentowania raportów.
Metodologia
Oracle w celu zwiększenia wydajności zaleca wykonanie wymienionych poniżej działań. Ważne jest, aby rozumieć strukturę zapytań MDX, a także dzienniki zapytań generowane przez Oracle Analytics.
Optymalizacja etapów wyboru
Zoptymalizowanie etapów wyboru pozwala uprościć zapytania MDX, ograniczyć liczbę generowanych zapytań MDX i poprawić wydajność.
Na poniższym rysunku pokazano przykładowe porównanie zoptymalizowanych i niezoptymalizowanych etapów wyboru.
Instrukcje CASE
Instrukcje CASE nie są obsługiwane w zapytaniach MDX i muszą być zawsze stosowane w Oracle Analytics. Wyjaśniona w tej sekcji logika dotycząca instrukcji CASE jest również prawdziwa dla większości funkcji, które nie są obsługiwane w zapytaniach MDX (takich jak if null itp.).
Używanie instrukcji CASE niesie ze sobą zarówno korzyści, jak i ograniczenia. Gdy instrukcje CASE są zawarte w formułach raportów, instrukcje te nie są uwzględniane w zapytaniu MDX. Może to uprościć zapytanie MDX i poprawić wydajność wykonywania. Wiąże się to jednak z ograniczeniem skuteczności filtrowania, co oznacza, że zapytanie może zwrócić więcej rekordów niż jest to potrzebne.
Korzystanie z funkcjonalności instrukcji CASE wiąże się z następującymi ograniczeniami:
CASE nie łączy wielu elementów, użyta w tej instrukcji kolumna podstawowa powinna być uwzględniona w zapytaniu i widokach jako oddzielna ukryta kolumna.CASE łączy wiele elementów, nie można uwzględnić kolumny podstawowej w widoku, nie wpływając przy tym na poziom agregacji. W takim przypadku:
SUM, MAX, MIN). Działa to tylko wtedy, gdy wewnętrzna reguła agregacji jest używana do łączenia elementów i dostarcza prawidłowe wyniki.Funkcja FILTER
W przeciwieństwie do instrukcji CASE funkcja FILTER może być dostarczana do bazy danych w celu wykonania.
Główną korzyścią płynącą z używania funkcji FILTER w formułach raportów jest to, że wybór jest stosowany w zapytaniu MDX, a objętość danych obliczanych i pobieranych z bazy danych zostaje ograniczona.
Główną wadą używania funkcji FILTER jest to, że może ona spowodować zwiększenie liczby wykonywanych zapytań MDX. Domyślnie dla każdej użytej funkcji FILTER jest wykonywane jedno zapytanie.
Przykładowe porównanie użycia instrukcji CASE i funkcji FILTER
W tym przykładzie użytkownik wnioskuje o raport przedstawiający zysk według kwartału i wybranego numeru SKU produktu. Dodatkowo numery SKU są pogrupowane w 12 kategorii. Do kategorii "Other Cola" są przypisane następujące produkty LOB: "Cola", "Diet Cola" i "Shared Diet Cola".
Zapytanie logiczne instrukcji CASE jest następujące:
SELECT
0 s_0,
CASE when XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" in ('Cola','Diet Cola','Shared Diet Cola') THEN 'Other Cola' ELSE XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" END s_1,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Product"."Category") s_2,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU") s_3,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Year"."Quarter") s_4,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Product"."Category") s_5,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU") s_6,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Year"."Quarter") s_7,
XSA('Admin'.'Sample.BasicPM')."Product"."Category" s_8,
XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" s_9,
XSA('Admin'.'Sample.BasicPM')."Year"."Quarter" s_10,
XSA('Admin'.'Sample.BasicPM')."Basic"."Profit" s_11
FROM XSA('Admin'.'Sample.BasicPM')
ORDER BY 8 ASC NULLS LAST, 11 ASC NULLS LAST, 5 ASC NULLS LAST, 2 ASC NULLS LAST, 7 ASC NULLS LAST, 10 ASC NULLS LAST, 4 ASC NULLS LAST, 6 ASC NULLS LAST, 9 ASC NULLS LAST, 3 ASC NULLS LAST
FETCH FIRST 125001 ROWS ONLYNie występuje grupowanie na podstawie instrukcji CASE. Generowane jest proste zapytanie MDX, przy czym instrukcja CASE jest przetwarzana przez Oracle Analytics:
With
set [_Product3] as 'Descendants([Product], [Product].Generations(3), leaves)'
set [_Year2] as 'Descendants([Year], [Year].Generations(2), leaves)'
select
{ [Measures].[Profit]
} on columns,
NON EMPTY {crossjoin({[_Year2]},{[_Product3]})} properties GEN_NUMBER, [Product].[MEMBER_UNIQUE_NAME], [Product].[Memnor], [Year].[MEMBER_UNIQUE_NAME], [Year].[Memnor] on rows
from [Sample.Basic]Instrukcja CASE jest wykonywana na serwerze BI i jest to odzwierciedlone w ustawieniu database 0:0,0 bazy danych:
RqList <<11777451>> [for database 0:0,0]
D1.c6 as c6 [for database 0:0,0],
D1.c4 as c4 [for database 0:0,0],
case when D1.c7 in ([ 'Cola', 'Diet Cola', 'Shared Diet Cola'] ) then 'Other Cola' else D1.c7 end as c2 [for database 0:0,0],
D1.c5 as c5 [for database 0:0,0],
D1.c3 as c3 [for database 0:0,0],
D1.c1 as c1 [for database 0:0,0],
D1.c7 as c7 [for database 0:0,0],
D1.c8 as c8 [for database 0:0,0]Alternatywnie można użyć filtra w odniesieniu do miary zysku, aby pobierać tylko wymagane elementy LOB. W tym scenariuszu tworzone są trzy miary z zastosowanymi odpowiadającymi im filtrami.
Zapytanie logiczne instrukcji FILTER jest następujące:
SELECT
0 s_0,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Product"."Category") s_1,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU") s_2,
DESCRIPTOR_IDOF(XSA('Admin'.'Sample.BasicPM')."Year"."Quarter") s_3,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Product"."Category") s_4,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU") s_5,
SORTKEY(XSA('Admin'.'Sample.BasicPM')."Year"."Quarter") s_6,
XSA('Admin'.'Sample.BasicPM')."Product"."Category" s_7,
XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" s_8,
XSA('Admin'.'Sample.BasicPM')."Year"."Quarter" s_9,
FILTER(XSA('Admin'.'Sample.BasicPM')."Basic"."Profit" USING XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" in ('Cola','Diet Cola','Shared Diet Cola')) s_10,
FILTER(XSA('Admin'.'Sample.BasicPM')."Basic"."Profit" USING XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" in ('Sasprilla','Birch Beer','Dark Cream')) s_11,
FILTER(XSA('Admin'.'Sample.BasicPM')."Basic"."Profit" USING XSA('Admin'.'Sample.BasicPM')."Product"."Product SKU" in ('xxxxx')) s_12
FROM XSA('Admin'.'Sample.BasicPM')
ORDER BY 7 ASC NULLS LAST, 10 ASC NULLS LAST, 4 ASC NULLS LAST, 6 ASC NULLS LAST, 9 ASC NULLS LAST, 3 ASC NULLS LAST, 5 ASC NULLS LAST, 8 ASC NULLS LAST, 2 ASC NULLS LAST
FETCH FIRST 125001 ROWS ONLYW tym scenariuszu generowane są trzy zapytania, po jednym dla każdego filtra, i występują problemy z wydajnością.
Zapytanie 1:
With
set [_Product3] as 'Filter([Product].Generations(3).members, ((IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "xxxxx")))'
set [_Year2] as 'Descendants([Year], [Year].Generations(2), leaves)'
select
{ [Measures].[Profit]
} on columns,
NON EMPTY {crossjoin({[_Year2]},{[_Product3]})} properties MEMBER_NAME, GEN_NUMBER, property_expr([Product], [MEMBER_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category_Null_Alias_Replacement"), property_expr([Product], [Default], Ancestor(currentaxismember(), [Product].Generations(2)), "Category"), property_expr([Product], [MEMBER_UNIQUE_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Member Key"), property_expr([Product], [Memnor], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Memnor"), [Product].[MEMBER_UNIQUE_NAME], [Product].[Memnor], [Year].[MEMBER_UNIQUE_NAME], [Year].[Memnor] on rows
from [Sample.Basic]
]]Zapytanie 2:
With
set [_Product3] as 'Filter([Product].Generations(3).members, ((IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Birch Beer") OR (IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Dark Cream") OR (IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Sasprilla")))'
set [_Year2] as 'Descendants([Year], [Year].Generations(2), leaves)'
select
{ [Measures].[Profit]
} on columns,
NON EMPTY {crossjoin({[_Year2]},{[_Product3]})} properties MEMBER_NAME, GEN_NUMBER, property_expr([Product], [MEMBER_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category_Null_Alias_Replacement"), property_expr([Product], [Default], Ancestor(currentaxismember(), [Product].Generations(2)), "Category"), property_expr([Product], [MEMBER_UNIQUE_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Member Key"), property_expr([Product], [Memnor], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Memnor"), [Product].[MEMBER_UNIQUE_NAME], [Product].[Memnor], [Year].[MEMBER_UNIQUE_NAME], [Year].[Memnor] on rows
from [Sample.Basic]
]]Zapytanie 3:
With
set [_Product3] as 'Filter([Product].Generations(3).members, ((IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Cola") OR (IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Diet Cola") OR (IIF(IsValid([Product].CurrentMember.MEMBER_ALIAS), [Product].CurrentMember.MEMBER_ALIAS, [Product].CurrentMember.MEMBER_Name) = "Shared Diet Cola")))'
set [_Year2] as 'Descendants([Year], [Year].Generations(2), leaves)'
select
{ [Measures].[Profit]
} on columns,
NON EMPTY {crossjoin({[_Year2]},{[_Product3]})} properties MEMBER_NAME, GEN_NUMBER, property_expr([Product], [MEMBER_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category_Null_Alias_Replacement"), property_expr([Product], [Default], Ancestor(currentaxismember(), [Product].Generations(2)), "Category"), property_expr([Product], [MEMBER_UNIQUE_NAME], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Member Key"), property_expr([Product], [Memnor], Ancestor(currentaxismember(), [Product].Generations(2)), "Category - Memnor"), [Product].[MEMBER_UNIQUE_NAME], [Product].[Memnor], [Year].[MEMBER_UNIQUE_NAME], [Year].[Memnor] on rows
from [Sample.Basic]Przykład zastosowanego filtra produktu
Lepszym podejściem jest uwzględnienie w raporcie kolumny produktu z pojedynczą kolumną miary bez filtra. Następnie należy utworzyć filtr uwzględniający wymagane produkty. Aby pogrupować produkty w różne kategorie, należy użyć instrukcji CASE. W tym scenariuszu jest generowane pojedyncze zapytanie MDX z odfiltrowanymi wierszami i nawet mimo tego, że w Oracle Analytics zastosowano instrukcję CASE, zapytanie używa podzbioru danych, a nie wszystkich rekordów.
Oto inny scenariusz, w którym instrukcje CASE powodują problemy z wydajnością.
Programista stosuje instrukcję CASE w celu zmiany nazw marek, a filtr monitujący pulpitu informacyjnego umożliwia użytkownikom wybór marki.
Ponieważ instrukcja CASE nie jest obsługiwana w MDX, nie można zastosować filtra Brand2 w zapytaniu MDX. Wybierane są wszystkie marki i nie jest to podejście zoptymalizowane.
W scenariuszach tego typu Oracle zaleca usunięcie instrukcji CASE i zmianę nazw elementów w bazie danych lub utworzenie aliasów.