W tym wpisie przyjrzymy się funkcji agregującej COLLECT. Funkcja ta przyjmuje jako argument kolumnę, zwracając w wyniku zagnieżdżoną tabelę dla każdej grupy, po której odbywa się grupowanie. Tabela ta zawiera wiersze złożone z wartości kolumny, która została podana jako argument. Wiersze zawarte w zagnieżdżonej tabeli, przynależą do grupy, dla której wywołana została funkcja (rys. 1.)
Pełny opis i składnia funkcji dostępna jest w dokumentacji:
https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/COLLECT.html#GUID-A0A74602-2A97-449B-A3EC-847D38D3DA90)
Przykładowy przypadek użycia 1: różne poziomy agregacji w jednym zapytaniu.
Funkcję COLLECT można użyć w celu zwrócenia w jednym zapytaniu wyników o różnych poziomach agregacji.
Na początku spójrzmy na przykład bez wykorzystania funkcji COLLECT. Chcemy obliczyć średnią pensję dla departamentu wg dwóch algorytmów:
- V1 – średnia pensja dla departamentu
- V2 – średnia pensja dla departamentu wyliczana ze średnich pensji dla poszczególnych stanowisk w ramach departamentu, np. jeśli w departamencie jest jeden kierownik, czterech sprzedawców i dwie osoby odpowiedzialne za administracje, to średnia V2 będzie średnią wyliczoną ze średniej pensji na stanowisku kierownika, średniej pensji na stanowisku sprzedawcy, średniej pensji na stanowisku administracyjnym.
select d.department_name ,to_char(avg(e.salary),'999G999D00') as dept_salary_avg_v1 ,(select to_char(avg(dept_job_salary_avg),'999G999D00') from ( select d_i.department_id ,e_i.job_id ,avg(e_i.salary) as dept_job_salary_avg from hr.employees e_i join hr.departments d_i on d_i.department_id = e_i.department_id where d_i.department_id = d.department_id group by d_i.department_id,e_i.job_id ) group by department_id ) as dept_salary_avg_v2 from hr.employees e join hr.departments d on d.department_id = e.department_id group by d.department_id, d.department_name order by 1;
Jak możemy zrobić to przy użyciu funkcji COLLECT? W tym celu utwórzmy dwa typy bazodanowe, które są potrzebne w kolejnym zapytaniu:
create or replace type job_salary as object ( job_title varchar2(35) ,salary number ) / create or replace type job_salary_tab as table of job_salary; /
Następnie możemy skorzystać z funkcji COLLECT:
select department_name ,to_char(dept_salary_avg_v1,'999G999D00') as dept_salary_avg_v1 ,(select to_char(avg(avg(salary)),'999G999D00') from table(dept_job_sal) group by job_title) as dept_salary_avg_v2 from ( select d.department_name ,avg(salary) as dept_salary_avg_v1 ,cast(collect(job_salary(job_title,salary)) as job_salary_tab) as dept_job_sal from hr.employees e join hr.jobs j on j.job_id = e.job_id join hr.departments d on d.department_id = e.department_id group by d.department_name ) order by 1;
Podobne rezultaty można oczywiście osiągnąć na różne sposoby. Niemniej funkcja COLLECT może być bardzo użyteczna np. gdy mamy duże skomplikowane zapytanie, w którym trudno szybko zmienić poziom agregacji. Dzięki wykorzystaniu funkcji COLLECT możemy zrobić to w stosunkowo prosty sposób, nie ingerujący mocno w główne zapytanie.
Przykładowy przypadek użycia 2: agregacja danych znakowych.
Ciekawym przypadkiem użycia jest agregacja danych znakowych z wykorzystaniem typów i funkcji z Oracle Application Express (APEX). Tym sposobem uzyskujemy efekt działania funkcji LISTAGG wraz z możliwością skorzystania z opcji DISTINCT. Funkcja LISTAGG nie pozwala na użycie w niej DISTINCT.
select d.department_name ,listagg(distinct j.job_title,', ') within group (order by j.job_title) as job_titles_1 ,apex_string.join(cast(collect(distinct j.job_title) as apex_t_varchar2),', ') as job_titles_2 from hr.employees e join hr.jobs j on j.job_id = e.job_id join hr.departments d on d.department_id = e.department_id group by d.department_name;
Autorzy:
Tomasz Strawski
Rafał Stryjek