Vistas em SQL são tabelas virtuais definidas sobre o resultado de consultas que podem envolver uma ou várias tabelas reais.
CREATE VIEW NY_EMPLOYEES_VIEW AS
SELECT
E.ENAME AS EMPLOYEE,
D.DNAME AS DEPARTMENT
FROM EMP E
INNER JOIN DEPT D ON E.DEPTNO = D.DEPTNO
WHERE D.LOC = 'NEW YORK';
O código acima permite definir uma vista sobre a consulta que rertorna o nome e o departamento de todos os funcionários de uma empresa que trabalham em Nova Iorque. Após a definição da vista, esta informação é obtida por intermédio de uma consulta simples.
SELECT EMPLOYEE, DEPARTMENT FROM NY_EMPLOYEES_VIEW;
A obtenção da mesma informação associada a todos os funcionários que trabalham numa localização arbitrária poder-se-ia realizar por intermédio da criação de uma vista para cada localização, o que constitui um método pouco prático quando o número de todas as localizações possíveis é elevado.
Uma alternativa consiste em definir a vista
CREATE VIEW GEN_LOC_EMPLOYEES_VIEW AS
SELECT
E.ENAME AS EMPLOYEE,
D.DNAME AS DEPARTMENT,
D.LOC AS LOCALIZATION
FROM EMP E
INNER JOIN DEPT D ON E.DEPTNO = D.DEPTNO;
e executar o código
SELECT EMPLOYEE, DEPARTMENT FROM GEN_LOC_EMPLOYEES_VIEW
WHERE LOCALIZATION = 'Chicago';
para obter, por exemplo, todos os funcionários que labutam em Chicago.
A execução da consulta
SELECT EMPLOYEE, DEPARTMENT FROM GEN_LOC_EMPLOYEES_VIEW
iria retornar a informação de todos os funcionários aos quais se encontra associado um departamento, independentemente da localização. No entanto, a obtenção de todos os resultados pode constituir um cenário que se pretende evitar, dada a quantidade de informção que é disponibilizada e, deste modo, a vista geral deverá ser evitada.
As funções de tabela são a alternativa óbvia. Para além de poderem implementar uma espécie de vistas parametrizadas, permitem encapsular código PL/SQL bem mais complexo.
Antes de entrar em detalhes realtivamente às funções que retornem estruturas das quais é possível extrair colecções de registos, é conveniente estabelecer, em primeiro lugar, um pequeno contexto que servirá como modelo. Para o efeito, crie-se a tabela BOOKS
CREATE TABLE BOOKS
(
ID INT NOT NULL
, TITLE VARCHAR2(1024) NOT NULL
, EDITOR VARCHAR2(256) NOT NULL
, ISBN VARCHAR2(64) NOT NULL
, CONTENT_DETAILS CLOB
, CONSTRAINT TABLE1_PK PRIMARY KEY (ID) ENABLE
);
cujos registos representam livros na qual o campo CONTENT_DETAILS é do tipo CLOB e contém um fragmento de XML que define a estrutura dos capítulos. Este cenário, além de ser plausível numa situação que requeira a consideração de funções de tabela permite ainda dar uma pequena introdução ao XML em base-de-dados.
São inseridas duas linhas na tabela para efeitos de teste.
INSERT INTO BOOKS(
ID,
TITLE,
EDITOR,
ISBN,
CONTENT_DETAILS
) VALUES(
1,
'A passion that goes behind treason',
'Ed, the editor',
'1234567890857',
'
<genre>
Novel
</genre>
<synopsis>
This book is about...
</synopsis>
<chapter number="1">
<title>When they met</title>
</chapter>
<chapter number="2">
<title>Their happy moments as a family</title>
</chapter>
<chapter number="3">
<title>Treason happens</title>
</chapter>
<chapter number="4">
<title>Forgiveness</title>
</chapter>
<chapter number="5">
<title>They''re happy again</title>
</chapter>
'
);
COMMIT;
INSERT INTO BOOKS(
ID,
TITLE,
EDITOR,
ISBN,
CONTENT_DETAILS
) VALUES(
2,
'Animals go crazy',
'The editor: Ed',
'0987654321783',
'
<genre>
Satire
</genre>
<synopsis>
This fable tells a story of...
</synopsis>
<chapter number="1">
<title>The animals in the farm</title>
</chapter>
<chapter number="2">
<title>The pig is elected president</title>
</chapter>
<chapter number="3">
<title>Every one is equal as a pig</title>
</chapter>
<chapter number="4">
<title>Snoring is the new language</title>
</chapter>
<chapter number="5">
<title>Chicken is arrested because she can''t snore</title>
</chapter>
<chapter number="6">
<title>The uprising of the donkeys</title>
</chapter>
<chapter number="7">
<title>The return to anarchy</title>
</chapter>
'
);
COMMIT;
Após a inserção das duas linhas linhas, a consulta
SELECT
X.CHAPTER_NUMBER,
X.CHAPTER_TITLE
FROM BOOKS B
CROSS JOIN XMLTABLE(
'/chapter'
PASSING XMLPARSE(CONTENT B.CONTENT_DETAILS WELLFORMED)
COLUMNS
CHAPTER_NUMBER INTEGER PATH '@number',
CHAPTER_TITLE VARCHAR2(1024) PATH 'title/text()'
) X
WHERE B.ID = :1
;
permite obter os capítulos do livro cujo ID é dado pelo parâmetro :1.
Algumas palavras sobre a consulta anterior podem torná-la mais clara. A instrução XMLPARSE permite gerar uma instância de XML a partir da coluna CONTENT_DETAILS. A palavra WELLFORMED permite evitar que o XML seja validado. A instrução XMLTABLE permite mapear o resultado da aplicação de uma consulta XQuery num sistema relacional de linhas e colunas. Neste caso, é passada, por intermédio da instrução PASSING, o XML carregado a partir da coluna CONTENT_DETAILS. O campo de texto ‘/chapter’ consiste numa expressão XQuery que retorna todos os elementos chapter descendentes da raiz. A instrução COLUMNS permite definir as colunas. O número do capítulo é dado pelo atributo number que se obtém directamente de cada elemento definido pela XQuery e a segunda coluna obtém-se a partir do texto associado ao elemento descendente title.
O objectivo que agora se coloca consiste em definir uma função que permita obter os capítulos de um livro definido por algum ID. Uma primeira abordagem consiste em abrir um cursor para a consulta e retornar uma referência.
CREATE OR REPLACE FUNCTION GET_BOOK_CHAPTERS
(
BOOK_ID IN INT
) RETURN SYS_REFCURSOR AS
cur SYS_REFCURSOR;
BEGIN
OPEN cur FOR
SELECT
X.CHAPTER_NUMBER,
X.CHAPTER_TITLE
FROM BOOKS B
CROSS JOIN XMLTABLE(
'/chapter'
PASSING XMLPARSE(CONTENT B.CONTENT_DETAILS WELLFORMED)
COLUMNS
CHAPTER_NUMBER INTEGER PATH '@number',
CHAPTER_TITLE VARCHAR2(1024) PATH 'title/text()'
) X
WHERE B.ID = BOOK_ID;
RETURN cur;
END GET_BOOK_CHAPTERS;
Para extrair os registos apontados pelo cursor retornado pela função recorre-se a código definido num bloco PL/SQL.
DECLARE
cur_res SYS_REFCURSOR;
TYPE RECTYPE IS RECORD (CHAPTER_NUMB INTEGER, CHAPTER_TITLE VARCHAR2(1024));
val RECTYPE;
BEGIN
cur_res := GET_BOOK_CHAPTERS(:1);
LOOP
FETCH cur_res INTO val;
EXIT WHEN cur_res%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(val.CHAPTER_NUMB || ' | ' || val.CHAPTER_TITLE);
END LOOP;
END;
De um modo geral, um cursor é obtido para os capítulos do livro identificado pelo ID com valor :1, é criado um ciclo no qual são obtidos sucessivamente os registos e o resultado é imprimido para a consola.
Identificam-se facilmente duas desvantagens neste método. A primeira, consiste no facto de que a obtenção dos registos a partir do cursor não se realiza por intermédio de SQL puro. A segunda, caracteriza-se pela dificuldade inerente ao cruzamento dos dados com os de outras tabelas.
A segunda dificuldade pode ser resolvida com a criação de uma tabela PL/SQL que pode ser incluída numa consulta de SQL puro. Neste caso, ao invés de se retornar o cursor, retorna-se uma tabela PL/SQL preenchida com os valores pretendidos. Para o efeito, é necessário definir, em primeiro lugar, o tipo de dados associados à tabela bem como a cada elemento.
CREATE OR REPLACE TYPE CHAPTER AS OBJECT( CHAPTER_NUMBER INTEGER, CHAPTER_TITLE VARCHAR2(1024));
CREATE OR REPLACE TYPE CHAPTERS_TABLE IS TABLE OF SERGIOMARQUES.CHAPTER;
A alteração da função anterior de modo a que seja retornada a tabela fica da seguinte forma.
CREATE OR REPLACE FUNCTION GET_BOOK_CHAPTERS_OBJ
(
BOOK_ID IN INT
) RETURN CHAPTERS_TABLE AS
cur SYS_REFCURSOR;
val1 INTEGER;
val2 VARCHAR2(1024);
tab CHAPTERS_TABLE;
rec CHAPTER;
i NUMBER := 1;
BEGIN
OPEN cur FOR
SELECT
X.CHAPTER_NUMBER,
X.CHAPTER_TITLE
FROM BOOKS B
CROSS JOIN XMLTABLE(
'/chapter'
PASSING XMLPARSE(CONTENT B.CONTENT_DETAILS WELLFORMED)
COLUMNS
CHAPTER_NUMBER INTEGER PATH '@number',
CHAPTER_TITLE VARCHAR2(1024) PATH 'title/text()'
) X
WHERE B.ID = BOOK_ID;
tab := CHAPTERS_TABLE();
LOOP
FETCH cur INTO val1, val2;
EXIT WHEN cur%NOTFOUND;
tab.EXTEND;
rec := CHAPTER(val1, val2);
tab(i) := rec;
i := i + 1;
END LOOP;
RETURN tab;
END GET_BOOK_CHAPTERS_OBJ;
Para executar a função anterior sobre o livro com ID dado por :1, é suficiente executar a seguinte consulta.
SELECT * FROM TABLE(GET_BOOK_CHAPTERS_OBJ(1));
Por seu turno, a grande vantagem deste tipo de funções consiste na possibilidade de ser executada para todos os livros numa única consulta sem a necessidade de proceder à criação explícita de um cursor.
SELECT
B.TITLE,
CHP.CHAPTER_NUMBER,
CHP.CHAPTER_TITLE
FROM BOOKS B
CROSS JOIN TABLE(SERGIOMARQUES.GET_BOOK_CHAPTERS_OBJ(B.ID)) CHP;
A consulta anterior permite consultar a tabela BOOKS e, para cada livro, obter a informação que define os capítulos. O operador TABLE permite manipular os elementos de uma colecção PL/SQL por intermédio de consultas SQL.
É possível simplificar a função anterior com o recurso à instrução BULK COLLECT que permite descarregar os resultados de uma consulta numa tabela PL/SQL.
CREATE OR REPLACE FUNCTION GET_BOOK_CHAPTERS_BULK
(
BOOK_ID IN INT
) RETURN CHAPTERS_TABLE AS
tab CHAPTERS_TABLE;
BEGIN
SELECT
CHAPTER(X.CHAPTER_NUMBER,X.CHAPTER_TITLE) AS REC
BULK COLLECT INTO tab
FROM BOOKS B
CROSS JOIN XMLTABLE(
'/chapter'
PASSING XMLPARSE(CONTENT B.CONTENT_DETAILS WELLFORMED)
COLUMNS
CHAPTER_NUMBER INTEGER PATH '@number',
CHAPTER_TITLE VARCHAR2(1024) PATH 'title/text()'
) X
WHERE B.ID = BOOK_ID;
RETURN tab;
END GET_BOOK_CHAPTERS_BULK;
Nenhuma diferença funcional existe entre as versões GET_BOOK_CHAPTERS_OBJ e GET_BOOK_CHAPTERS_BULK. A consulta
SELECT
B.TITLE,
CHP.CHAPTER_NUMBER,
CHP.CHAPTER_TITLE
FROM BOOKS B
CROSS JOIN TABLE(GET_BOOK_CHAPTERS_BULK(B.ID)) CHP;
permite obter os mesmos resultados que a sua contraparte.
Os principais inconvenientes desta abordagem advêm do facto de que a função só retorna os registos após a sua inserção num colecção em memória. No caso em que a quantidade de dados é elevada ou alguma transformação tenha de ser aplicada sobre os dados, é certo que o preencimento da totalidade da colecção poderá ser demorado. Para mitigar o problema entram em cena as funções canalizadas ou PIPELINED.
CREATE OR REPLACE FUNCTION GET_BOOK_CHAPTERS_PIPELINED
(
BOOK_ID IN INT
) RETURN CHAPTERS_TABLE PIPELINED PARALLEL_ENABLE AS
CURSOR cur IS
SELECT
CHAPTER(X.CHAPTER_NUMBER, X.CHAPTER_TITLE)
FROM SERGIOMARQUES.BOOKS B
CROSS JOIN XMLTABLE(
'/chapter'
PASSING XMLPARSE(CONTENT B.CONTENT_DETAILS WELLFORMED)
COLUMNS
CHAPTER_NUMBER INTEGER PATH '@number',
CHAPTER_TITLE VARCHAR2(1024) PATH 'title/text()'
) X
WHERE B.ID = BOOK_ID;
rec CHAPTER;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN cur%NOTFOUND;
PIPE ROW(rec);
END LOOP;
CLOSE cur;
END;
Observe-se que a função é marcada como PIPELINED. Neste caso, cada registo é obtido de um cursor e despachado, por intermédio da instrução PIPE, para o cliente da função.
SELECT * FROM TABLE(GET_BOOK_CHAPTERS_PIPELINED(:1));
Quando a consulta é executada sobre a chamada da função, esta processa os registos à medida que são despachados pela instrução PIPE. Tal padrão, vulgarmente designado por Produtor/Consumidor, permite o respectivo tratamento por várias linhas de execução independentes, aumentando deste modo o grau de paralelismo. Se for pretendida a utilização de paralelismo, é necessário marcar a função como PARALLEL_ENABLE.
SELECT /*+ PARALLEL(B, 4) */
B.TITLE,
P.CHAPTER_NUMBER,
P.CHAPTER_TITLE
FROM SERGIOMARQUES.BOOKS B
CROSS JOIN TABLE(SERGIOMARQUES.GET_BOOK_CHAPTERS_PIPELINED(B.ID)) P;
Dada a sua flexibilidade, as funções canalizadas adequam-se ao desenvolvimento de procedimentos para transformação massiva de dados durante a sua transferência a partir da base-de-dados operacional.