Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Structured Query Language, ou Linguagem de Consulta Estruturada ou SQL, é uma linguagem de pesquisa
declarativa para bancos de dados relacionais. Com o advento dos primeiros produtos baseados no modelo de dados
relacional (no final dos anos 70, implementando a teoria proposta pelo pesquisador E. F. Codd alguns anos antes),
surgiu a necessidade de se desenvolver uma linguagem voltada especificamente para este uso. O nome original da
linguagem era SEQUEL, acrônimo para "Structured English Query Language" (Linguagem de Consulta Estruturada em
Inglês), vindo daí o fato de, até hoje, a sigla, em inglês, ser comumente pronunciada "síquel" ao invés de "és-kiú-él",
letra a letra. No entanto, em português, a pronúncia mais corrente é a letra a letra: "ésse-quê-éle".
Originalmente a linguagem só possuía instruções para pesquisa de dados, mas em pouco tempo foram introduzidos
comandos para inserção/deleção/alteração de dados (DML, Data Manipulation Language, tais como INSERT,
UPDATE, DELETE), para definição de estruturas como tabelas/índices/etc (DDL, Data Definition Language, tais como
CREATE, ALTER e DROP), para controle/autorização de acesso aos dados (DCL, Data Control Language, tais como
GRANT e REVOKE) e de controle de transação (DTL, Data Transaction Language, tais como ROLLBACK e COMMIT). O
comando original para pesquisa de dados (SELECT) é um caso á parte, pois originalmente foi incluído num grupo
único para ele, chamado DQL (Data Query Language), mas diversos fabricantes em breve deram ao comando a
capacidade de travar/lockar os registros que recuperou (via cláusula FOR UPDATE ou similar), o que implica
alteração e portanto incluiria o SELECT no grupo de DDLs.
Desta forma, neste Treinamento vamos utilizar em nível pleno as facilidades e recursos no dialeto SQL do banco de
dados Oracle, visando à máxima performance e eficiência do código contra um banco Oracle.
A linguagem SQL é o único meio oficial de um cliente qualquer obter/alterar/remover informações de um banco de
dados (já que o aceso direto aos datafiles é proibido), assim qualquer que seja o cliente, em qualquer ambiente, o
mesmo deverá obter uma conexão (normalmente via rede), e por meio desta enviar um texto com o SQL desejado,
que será validado, executado e armazenado no banco Oracle, e os dados enviados de volta ao cliente. Até mesmo
outras linguagens que podem coexistir no database Oracle (como PL/SQL e Java) ao necessitarem
manipular/consultar dados, alterar estruturas do database, etc, Necessariamente executam comandos SQL, que
serão enviados ao SQL Engine (o componente especializado em interpretar SQL).
Assim que o banco de dados Oracle recebe um SQL, o método para o SQL ser processado é :
Passo 1: é checado se o SQL é válido, utiliza comandos válidos na sintaxe correta, se não retorna o erro ORA-xxx
correspondente
Passo 2: checada a semântica, ie, se os objetos que o SQL referencia existem, se o usuário que submeteu o SQL tem
acesso a eles, etc : se check falhou, exibir erro ORA-xx apropriado
Passo 3: no texto integral do SQL submetido é aplicada uma função interna de hash, reduzindo-o a um código
numérico de hash
Passo 5: se o valor de hash não foi encontrado, é um hard parse, vá para passo 8
Passo 6: valor de hash foi encontrado, se o texto do SQL em análise difere do texto no cache, fazer hard parse, vá
para passo 8, caso contrário será necessária apenas uma breve verificação de status dos objetos referenciados no
SQL e no plano de execução existentes em cache, é o chamado SOFT PARSE
Passo 7: se SOFT PARSE foi bem sucedido, textos em cache e do SQL em análise são idênticos, se os objetos
referenciados no SQL em cache ainda são os mesmos e não houve alterações de ambiente, vá para passo 9 executar
o plano de acesso, caso contrário houve alteração de ambiente, forçar um hard parse indo para passo 8
Passo 8: hard parse, o texto do SQL vai para o cache, o otimizador (CBO – Cost Based Optimizer) entra em cena, o
SQL é analizado, diversos planos de execução são montados e avaliados, o melhor é escolhido (melhor aqui
entendido como aquele que na avaliação do CBO apresentará o menor Custo, ie, gastará menos CPUs, memória,
qtdade de blocos acessados, etc) – esta Avaliação é basicamente feita pelo CBO com informações coletadas
anteriormente, como quantidade de linhas, domínio dos valores presentes, etc . Os demais planos considerados são
descartados. Este passo é EXTREMAMENTE custoso, assim deve ser evitado, re-aproveitando-se SQLs sempre que
possível (ver BIND VARIABLES, abaixo)
Passo 9: executar o plano de acesso, e depois da execução manter o mesmo em cache, de modo que eventuais re-
execuções do exato mesmo texto deste SQL usem o mesmo plano.
DEMONSTRAÇÃO #1 :
Objetivo : exemplificar o mecanismo de parse e montagem de planos e demonstrar o efeito de cache e as estatísticas
de performance e monitoração dos SQLs built-in.
Session altered.
Query que retorna apenas uma linha, veremos que o CBO optará por uso de índice :
Query idêntica mas com elemento de filtragem indeterminado mas possivelmente múltiplas linhas, o CBO irá optar por
um FULL TABLE SCAN , dado o tamanho relativamente pequeno da tabela :
HR:@orcl:SQL> select employee_id, first_name, last_name, salary from employees where DEPARTMENT_ID>0;
SYS:AS SYSDBA@orcl:SQL> select sql_id, sql_text, elapsed_time, buffer_gets, rows_processed from v$sql
where sql_text like 'select department_id, last_name, first_name, salary from employees where
DEPARTMENT_ID%';
SQL_ID
-------------
SQL_TEXT
--------------------------------------------------------------------------------------------------
fcgyhzfzwnkbn
3299 26 106
99htt8hdqgwbv
7377 17 1
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------
SQL_ID 99htt8hdqgwbv, child number 0
-------------------------------------
select department_id, last_name, first_name, salary from employees
where DEPARTMENT_ID=10
24 rows selected.
=> Plano de Execução demonstrando table scan :
ABLE_OUTPUT
-------------------------------------------------------------------------------------------------------
--------------
fcgyhzfzwnkbn, child number 0
-------------------------------
department_id, last_name, first_name, salary from employees
DEPARTMENT_ID>0
-----------------------------------------------------------------------------------
| Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------
| SELECT STATEMENT | | 1 | | 106 |00:00:00.01 | 14 |
| TABLE ACCESS FULL| EMPLOYEES | 1 | 106 | 106 |00:00:00.01 | 14 |
-----------------------------------------------------------------------------------
filter("DEPARTMENT_ID">0)
selected.
SYSDBA@orcl:SQL>
Funcionamento do CBO
onalidade básica do CBO consiste em estimar o Custo (ie, o esforço/tempo/recursos gastos) nos diversas métodos possíveis
so aos dados para poder escolher o mais eficiente. O principal indicador é a quantidade de linhas esperadas a serem
adas pelo SQL.
emos que numa apresentação há uma platéia de 1200 pessoas. Digamos que precisamos estimar quantas pessoas nasceram
de Dezembro – ora, se não tivermos infomação em contrário, suporemos que em cada mês do ano nascem o mesmo
o de pessoas, assim sabendo que há 12 meses no ano, a chance de uma pessoa nascer num mês qualquer (inclusive em
bro, que é o que nos interessa) é de 1 em 12, ou de 1/12 avos, que é aproximadamente igual a 0,0834. Assim, do total de
essoas, a estimativa seria de 1200 * 0,0834, que é 100. Assim, na falta de informações mais detalhadas, responderíamos que
soas nasceram em Dezembro.
emplo, embora simples, ilustra bem precisamente o comportamento básico do CBO, que supõe que os dados estão
ídos mais ou menos regularmente, não havendo nenhum valor possível com muito mais ou muito menos ocorre.
e continuarmos, porém, primeiro vamos empregar a nomenclatura adequada dos itens referentes à análise do CBO:
Datas de nascimento estão igualmente distribuídas pelos meses -- distribuição dos dados
1/12 da platéia nasceu em um dado mês -- seletividade (ie, qual fração do total será acessada) –
e 1 , mas normalmente expressa em percentual
:@orcl:SQL>BEGIN
for r in 1..12 loop
insert into PLATEIA (select seq_pessoa.nextval, chr(65+r) || object_name, r
from all_objects where rownum < 101);
end loop;
commit;
D;
etaremos estatísticas solicitando análise de valores apenas para as colunas indexadas (que não
m), portanto o CBO vai “chutar” esses valores :
ORTANTE : por default somente são registradas num Plano de Execução as informações mais básicas : para
os as informações Extendidas do plano, tais como linhas estimadas e processadas, devemos Ativar a
completa de estatísticas de SQL :
n altered.
T(*)
----
100
:@orcl:SQL>select sql_id from v$sql where sql_text like 'SELECT COUNT(*) FROM PLATEIA WHERE
IV=12%';
-------
18umt04
----------------------------------------------------------------------------------------------------
-------------------------------
----------------------------------------------------------------------------------
| Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------
| SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 |
| SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 5 |
| TABLE ACCESS FULL| PLATEIA | 1 | 12 | 100 |00:00:00.01 | 5 |
----------------------------------------------------------------------------------
---------------------------------------------
filter("MES_ANIV"=12)
s selected.
estimativa de linhas do CBO passo MUITO LONGE da real : existem MUITO MAIS que 12 linhas, por que isso
u ? Porque o CBO ** NÃO TINHA ** a informação de que só há 12 meses possíveis – ele possui a informação
de linhas da tabela (cardinalidade-base) :
o Procedimento para se coletar informação sobre a Cardinalidade é através de uma coleta de estatísticas que incluam a(s)
una(s) em questão : no exemplo, coletaremos estatísticas sobre todas as colunas, mas é Totalmente possível coletar para
umas colunas apenas :
SYSTEM:@orcl:SQL>
(a informação sobre o domínio/valores possíveis coletados fica armazenada em raw, portanto vamos
criar uma pequena procedure para fazer a conversão) :
Function created.
ora sim o CBO possui a informação necessária, ele “sabe” que há 12 meses possíveis, portanto (ainda supondo distribuição
al) qualquer valor usado como filtro deve retornar 1/12 do total de 1200 linhas :
(primeiro, vamos executar o SQL anterior com alterações mínimas no texto para que seja feito um novo PARSE e
portanto montado um novo Plano, agora se aproveitando das informações adicionais)
RESULT
------------------------
100
SYSTEM:@orcl:SQL>select sql_id from v$sql where sql_text like 'SELECT COUNT(*) RESULT FROM
PLATEIA WHERE MES_ANIV=12%';
SQL_ID
-------------
g7bumkdzhs4c1
=> eis o resultado, ver que agora o CBO fez a estimativa adequada :
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------
---------------------------------
SQL_ID g7bumkdzhs4c1, child number 0
-------------------------------------
SELECT COUNT(*) RESULT FROM PLATEIA WHERE MES_ANIV=12
----------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 5 |
|* 2 | TABLE ACCESS FULL| PLATEIA | 1 | 100 | 100 |00:00:00.01 | 5 |
----------------------------------------------------------------------------------------
2 - filter("MES_ANIV"=12)
19 rows selected.
SYSTEM:@orcl:SQL>
Avançando um pouco mais : uma das principais pontos do CBO é que,sem informação extra, da única maneira que
ele pode ele supõe que a distribuição é regular, ie, que cada um dos valores distintos de uma coluna retorna mais ou
menos o mesmo número de linhas se usado como filtro num WHERE – no caso recém-apresentado isso era verdade,
vamos alterar o caso para que não o seja mais :
SYSTEM:@orcl:SQL>insert into PLATEIA (select object_id, object_name, 12 from all_objects);
Commit complete.
Index created.
MES_ANIV COUNT(*)
---------- ----------
1 100
6 100
11 100
2 100
4 100
5 100
8 100
3 100
7 100
9 100
10 100
12 73659
12 rows selected.
==> ou seja, a distribuição deixou de ser uniforme. Vamos re-calcular estatísticas SEM informar
isso :
a opção de SIZE acima indica quantas "amostras" para cada valor distinto devem ser tomadas,
se especificado 1, uma única amostra não serve para indicativo, na prática é desprezada e o CBO
considera distribuição regular, e a opção de NO_INVALIDATE desativada faz com que os planos
anteriores que dependiam das estatísticas antigas sejam invalidados, é o que queremos neste
caso. Vamos ver o comportamento do CBO :
SYSTEM:@orcl:SQL>select sql_id from v$sql where sql_text like 'SELECT COUNT(*) RESULT FROM
PLATEIA WHERE MES_ANIV=12%';
SQL_ID
-------------
g7bumkdzhs4c1
SYSTEM:@orcl:SQL>select * from table(dbms_xplan.display_cursor('g7bumkdzhs4c1', null,
'ALLSTATS'));
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------
---------------------------------
SQL_ID g7bumkdzhs4c1, child number 0
-------------------------------------
SELECT COUNT(*) RESULT FROM PLATEIA WHERE MES_ANIV=12
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.11 | 146 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.11 | 146 |
|* 2 | INDEX RANGE SCAN| IDX_PLATEIA | 1 | 6230 | 73659 |00:00:00.06 | 146 |
-------------------------------------------------------------------------------------------
2 - access("MES_ANIV"=12)
19 rows selected.
SYSTEM:@orcl:SQL>
==> mais uma vez, estimativa de linhas TOTALMENTE off, o select anterior já nos mostrou que há
muitas mais, esse Cardinalidade está longe da real, ** E ** ( o mais importante) devido à isso o
CBO optou por fazer um INDEX RANGE SCAN, ie, vai ler um a um as chaves do índice que se
encontram no intervalo : isso seria ótimo se estivéssemos realmente recuperando poucas linhas,
mas no caso de um grande universo de linhas, muito mais eficiente seria ser (em operação
multibloco) os extents inteiros do índice, de maneira similar ao FULL TABLE SCAN...
Vamos ofertar ao CBO a informação de distribuição de valores, criando HISTOGRAMAS (que é o nome
técnico desse levantamento de distribuição de valores distintos em colunas), isso se faz
colocando um SIZE não-único e veremos que a estimativa será ajustada de acordo :
RESULT
------------------------
73,659
SYSTEM:@orcl:SQL>select sql_id from v$sql where sql_text like 'SELECT COUNT(*) RESULT FROM
PLATEIA WHERE MES_ANIV=12%';
SQL_ID
-------------
g7bumkdzhs4c1
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------
---------------------------------
SQL_ID g7bumkdzhs4c1, child number 0
-------------------------------------
SELECT COUNT(*) RESULT FROM PLATEIA WHERE MES_ANIV=12
-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.08 | 154 |
| 1 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.08 | 154 |
|* 2 | INDEX FAST FULL SCAN| IDX_PLATEIA | 1 | 73659 | 73659 |00:00:00.04 | 154 |
-----------------------------------------------------------------------------------------------
2 - filter("MES_ANIV"=12)
19 rows selected.
OBS : a cláusula AUTO utilizada acima para o SIZE dos histogramas tenta fazer uma coleta mediana, nem muito curta
nem muito completa – em alguns casos ela pode não fornecer uma amostra estatisticamente significativa, aí se
deverá fornecer um tamanho diretamente. Quanto maior o SIZE mais amostras serão coletadas, o que vai demandar
mais tempo e processamento/esforço por parte do RDBMS. Devido à isso , o limite máximo de tamanho para
histograma é 254.
Bind variables : conforme já apresentado anteriormente, o reconhecimento de um SQL já presente no cache de SQLs
para fins de re-utilização do plano é através da comparação do texto do SQL – assim sendo, além de padronização
num ambiente (para que não se termine com múltiplos SQLs diferentes apenas por espaços, ordem das colunas,
comentários e etc), é vital, caso o ambiente seja do tipo OLTP (ie, com muitas e múltiplas instruções SQL
simultâneas) que se utilize o recurso de BIND VARIABLEs , a fim de evitarmos SQLs diferentes apenas nos valores de
entrada.
Exemplo : digamos que necessitamos consultar os empregados com os códigos 100, 101 e 102 , vejamos o
comportamento do RDBMS se eu informar os valores diretamente :
HR:@orcl:SQL>
ou seja , como havia a mínima diferença devido aos valores diferentes, cada Execução foi considerada um
SQL diferente, portanto cada uma precisou de um PARSE, cada uma ocupou memória no cache de SQL, não
houve reaproveitamento....
Agora vamos aplicar o conceito de BIND VARIABLE, criando uma variável no programa/linguagem/tool de
programação cliente em uso e referenciando essa variável no texto do SQL, que será sempre o mesmo – no caso
do sqlplus, que estamos usando aqui, o comando apropriado é o comando VARIABLE, mas necessariamente a
Esmagadora maioria das tools/linguagens de programação Possui algum comando similar :
HR:@orcl:SQL>print V_ID
V_ID
----------
100
resultado :
SYSTEM:@orcl:SQL>
apenas um SQL foi armazenado no cache, e o plano gerado para esse único foi re-aproveitado.
OBS : no módulo de PL/SQL, será referenciado que não só automaticamente os textos SQL presentes em
programas PL/SQL já são convertidos para maiúsculas e tem espaços em branco redundantes eliminados
como Também toda variável PL/SQL citada neles será usada como BIND VARIABLE automaticamente – assim
sendo, uma Opção a se considerar se a tool/linguagem de programação apresentar dificuldades para
BINDING e para padronização de SQLs, é encapsular os comandos SQL necessários em stored PL/SQL , sejam
procedures, Functions ou Packages.
===============================================================================================
=============================================
Definição de um Plano de Execução : um Plano de Execução pode ser definido como uma sequência de passos (cada
passo definido como uma Operação lógica num database). Cada passo produz um rowset que será enviado ao passo
hierarquicamente superior, até que o rowset final seja encontrado e enviado ao cliente/solicitante. Planos de
Execução são comumente representados em formato tabular, com as operações-pai indentadas à esquerda das
operações-filho que as alimentam, mas podem ser também representados por uma árvore.
Exemplo - digamos que eu tenha a query abaixo (a hint de obtenção de estatísticas de execução do plano é apenas
para obtermos os detalhes extendidos no plano de Execução) :
HR:@orcl:SQL> select d.DEPARTMENT_NAME, min(e.salary), max(e.salary), count(*)
from departments d, employees e
where d.department_id = e.department_id
and d.department_id = 60
* group by d.department_name;
HR:@orcl:SQL>
vamos verificar o Plano de Execução – o primeiro passo é localizar o ID, a sequência alfanumérica que todo
SQL residente no cache possui :
SQL_ID PREV_SQL_ID
------------- -------------
5150gs690qykc
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------------------------
-------
SQL_ID 5150gs690qykc, child number 0
-------------------------------------
select d.DEPARTMENT_NAME, min(e.salary),
max(e.salary), count(*) from departments d, employees e where
d.department_id = e.department_id and d.department_id = 60 group by
d.department_name
------------------------------------------------------------------------------------------------
---
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time
|
------------------------------------------------------------------------------------------------
---
| 0 | SELECT STATEMENT | | | | 1 (100)|
|
| 1 | HASH GROUP BY | | 5 | 280 | 1 (0)|
00:00:01 |
| 2 | NESTED LOOPS | | 5 | 280 | 1 (0)|
00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 1 | 30 | 0 (0)|
|
|* 4 | INDEX UNIQUE SCAN | DEPT_ID_PK | 1 | | 0 (0)|
|
| 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 5 | 130 | 1 (0)|
00:00:01 |
|* 6 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 5 | | 0 (0)|
|
------------------------------------------------------------------------------------------------
---
Predicate Information (identified by operation id):
---------------------------------------------------
4 - access("D"."DEPARTMENT_ID"=60)
6 - access("E"."DEPARTMENT_ID"=60)
Note
-----
- dynamic sampling used for this statement (level=2)
31 rows selected.
SYS:AS SYSDBA@orcl:SQL>
(para que a identação fique mais visível, vamos traçar linhas coloridas nos níveis) :
------------------------------------------------------------------------------------------------
---
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time
|
------------------------------------------------------------------------------------------------
---
| 0 | SELECT STATEMENT | | | | 1 (100)|
|
| 1 | HASH GROUP BY | | 5 | 280 | 1 (0)|
00:00:01 |
| 2 | NESTED LOOPS | | 5 | 280 | 1 (0)|
00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| DEPARTMENTS | 1 | 30 | 0 (0)|
|
|* 4 | INDEX UNIQUE SCAN | DEPT_ID_PK | 1 | | 0 (0)|
|
| 5 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 5 | 130 | 1 (0)|
00:00:01 |
|* 6 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 5 | | 0 (0)|
|
------------------------------------------------------------------------------------------------
---
no exemplo acima, o comando cujo resultado vai ser enviado ao solicitante(o SELECT, passo 0) recebe os
resultados do HASH GROUP BY (passo 1), já que o passo 1 está endentado em relação à ele (linha azul) . No
nível abaixo do HASH GROUP BY (linha vermelha) encontramos um NESTED LOOP (um loop de repetição),
indicando que os passos endentados em relação à ele vão ser repetidos (os passos abaixo do nível indicado
pela linha amarela). Os alimentadores do NESTED LOOP (passos 3 e 5, os TABLE ACCESS) estão no mesmo
nível (isso está indicado pela linha verde) , o que implica que são independentes entre si , podendo inclusive
ser executados simultaneamente em paralelo pelo RDBMS. Os passos mais endentados em relação a cada
TABLE ACESS são os seus alimentadores (passos 4 e 6).
Lendo o plano logicamente, o pseudocódigo ficaria :
a. início do loop
b. se ainda não foi feita, fazer uma busca única (INDEX UNIQUE) no índice DEPT_ID_PK aonde
DEPARTMENT_ID=60 (cláusula 4 - access("D"."DEPARTMENT_ID"=60) de filtro, acima), e com
o valor encontrado acessar o registro correspondente na tabela DEPARTMENTS – armazenar
a informação encontrada numa tabela temporária, em memória, que o RDBMS vai construir
para isso (uma HASH TABLE)
c. ler o valor corrente (ou o primeiro valor, se começou agora) do índice
EMP_DEPARTMENT_IX aonde DEPARTMENT_ID=60 (claúsula 6 - access("E"."DEPARTMENT_ID"=60),
acima) , e com esse valor buscar o registro na tabela EMPLOYEES – armazenar a
informação encontrada numa tabela temporária, em memória que o RDBMS vai construir
para isso (uma HASH TABLE)
d. como é um INDEX RANGE SCAN, não único, pode haver N registros com a chave especificada :
movimentar o ponteiro lógico para a próxima ocorrência de DEPARTMENT_ID=60 no índice
EMP_DEPARTMENT_IX
e. se não finalizou a leitura do índice, goto a., se sim quebrar o loop
f. após o loop encerrado, os resultados vão estar nas hash tables, que alimentarão o passo
hierarquicamente superior (o HASH GROUP BY), que será o encarregado de processar as hash tables e
agrupar a informação
g. a informação agrupada alimenta o SELECT, que enviará o resultset final para o cliente
SELECT
HASH GROUP BY
TABLE ACCESS DEPARTMENTS via INDEX TABLE ACCESS
EMPLOYEES via INDEX