Mostrando postagens com marcador Oracle. Mostrar todas as postagens
Mostrando postagens com marcador Oracle. Mostrar todas as postagens

quinta-feira, 26 de abril de 2018

Escondendo Colunas no Oracle

Suponha que uma tabela RH.USUARIOS tenha uma coluna SENHA que, por qualquer motivo, não pode transformada num hash. É mais seguro delimitar quem pode ver os dados dessa coluna.

O primeiro passo é criar uma função guardiã para definir as situações em que os dados podem ser vistos. A função abaixo permite apenas que as consultas feitas a partir dos esquemas RH e XY possam ver o que há na coluna. Consultas feitas a partir de outros esquemas verão apenas null.

create or replace function 
hide_col( p_owner in varchar2, p_name in varchar2 ) 
return varchar2
as
begin
  if sys_context( 'userenv', 'session_user' ) in ('RH', 'XY')  then
    return null;
  else
    return '1=0';
  end if;
end;


O valor retornado pela função é usado como uma cláusula de um where, então null vai liberar o acesso. Esse mecanismo permite criar políticas mais complexas, mas neste caso apenas restringimos por esquema.

Depois, é preciso registrar a política de segurança com DBMS_RLS:

BEGIN
  DBMS_RLS.ADD_POLICY(object_schema=>'RH', object_name=>'USUARIO',
    policy_name=>'USUARIO_SENHA',
    function_schema=>'RH',
    policy_function=>'hide_col',
    sec_relevant_cols=>'SENHA',
    sec_relevant_cols_opt=>dbms_rls.ALL_ROWS);
END;


E agora as senhas estão protegidas.

sexta-feira, 6 de abril de 2018

Inflação Acumulada Com Funções Analíticas

Imagine ter uma tabela com os valores de inflação mês a mês:

create table ipca (
  data date,
  inflacao number(5,2)
);

Mas, além da inflação do mês, é preciso calcular a inflação acumulada dos últimos doze meses. Com funções analíticas, isso deve ser fácil. Entretanto, o SQL oferece sum() e count(), mas não uma função de agregação que multiplique os valores. Com um pouquinho de esperteza e logaritmos, isso pode ser superado. O segredo é somar os logaritmos e depois tirar o exponencial.

select data, inflacao, (exp(acumulada)-1)*100 from (
  select data, inflacao, 
    sum(ln(1+(inflacao/100)))
      over (order by data desc rows between current row and 11 following)
        as acumulada
  from ipca
)

A janela de classificação é autoexplicativa: ela ordena os índices por data decrescente e olha o registro atual e os onze anteriores a ele.

Um índice 0,23% vira 1,0023 (1+0,23/100) e um índice -0,05% vira 0,9995 (1-0,05/100). Por isso, substrai-se 1 do exponencial. Se o índice for 0, o 1 o neutraliza. A soma dos logaritmos desses valores corresponde à multiplicação.

sexta-feira, 15 de dezembro de 2017

Docker com DBD::Oracle

São poucas as aplicações que não requerem conexão a um banco de dados. Então, após conseguir criar uma imagem de Docker com Perl e alguns pacotes adicionais, resolvi experimentar algo mais complicado: instalar o DBD::Oracle. Este pacote já é naturalmente difícil de instalar, mas com alguma experimentação, descobri uma maneira rápida e simples de resolver este problema.

Em primeiro lugar, é preciso buscar os rpms do cliente da Oracle. Inicialmente, usei os mais modernos (versão 12.2), mas estes não tinham tudo que o DBD::Oracle verifica na fase de testes (e a instalação falha). Então, usei os seguintes arquivos da versão 11.2:
  • oracle-instantclient11.2-devel-11.2.0.3.0-1.x86_64.rpm
  • oracle-instantclient11.2-basic-11.2.0.3.0-1.x86_64.rpm
  • oracle-instantclient11.2-sqlplus-11.2.0.3.0-1.x86_64.rpm
Dentro da pasta do projeto, é preciso extrair os arquivos dos rpms, desta maneira: 

rpm2cpio oracle-instantclient11.2-devel-11.2.0.3.0-1.x86_64.rpm | cpio -idmv
rpm2cpio oracle-instantclient11.2-basic-11.2.0.3.0-1.x86_64.rpm | cpio -idmv
rpm2cpio oracle-instantclient11.2-sqlplus-11.2.0.3.0-1.x86_64.rpm | cpio -id/usr/lib64/libaio.so.1.0.1mv

A ordem não é importante. O resultado será uma pasta usr/ com várias subpastas e diversos arquivos ocupando cerca de 183MB.

Adicionalmente, é preciso copiar a bliblioteca libaio (Asynchronous I/O) do /usr/lib64 desta máquina para o usr/lib64 da pasta do projeto. Provavelmente haverá um link simbólico chamado libaio.so.1 apontando a um arquivo  libaio.so.1.0.1. Eu simplesmente copiei o arquivo com o nome libaio.so.1.

O Dockerfile requer apenas que seja adicionada essa pasta e que sejam preparadas as variáveis de ambiente.

FROM        perl:latest
MAINTAINER  forinti

ENV ORACLE_HOME /usr/lib/oracle/11.2/client64
ENV PATH $PATH:$ORACLE_HOME
ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:$ORACLE_HOME/lib:/usr/lib64

COPY usr/ /usr/
RUN curl -L http://cpanmin.us | perl - App::cpanminus
RUN cpanm DBI
RUN cpanm -v DBD::Oracle
RUN cpanm HTML::Parser
RUN cpanm Dancer2

EXPOSE 3000

CMD perl /app/hello.pl


As três linhas com ENV ajustam os valores das variáveis de ambiente. A opção -v na linha de instalação do DBD::Oracle faz com que todo o andamento da instalação seja impresso na tela. Sem essa opção, o cpanm escreve num arquivo de log que acaba sendo perdido quando ele falha e o docker termina.

Testes simples comprovaram que o driver funciona.

segunda-feira, 30 de janeiro de 2017

Conectando Postgresql a Oracle com Perl

Postgresql oferece a possibilidade de mapear tabelas e vistas via FDWs (Foreign Data Wrappers). Isso, por si só, já é muito útil. Entretanto, melhor ainda seria poder invocar rotinas. Como o Postgresql permite escrever rotinas em Python, TCL, e Perl, a saída é óbvia.


create or replace 
function call_remote_function(varchar, varchar[]) returns varchar as $$
  use DBI;
  use DBD::Oracle;
  
  my $proc=shift;
  $proc=~s/[^A-Za-z0-9_.]//g;
  my $vars=shift;
  my $num_vars=scalar(@{$vars});
  my $placeholders=join(',',('?')x$num_vars);
    
  my $host=$ENV{'REMOTE_HOST'};
  my $schema=$ENV{'REMOTE_SCHEMA'};
  my $sid=$ENV{'REMOTE_SID'};
  my $pass=$ENV{'REMOTE_PASS'};
  
  my $db = DBI->connect("dbi:Oracle:host=$host;sid=$sid;server=POOLED", 
                        "$schema", "$pass", {ora_drcp=>1,ora_drcp_class=>'SHARED'})
    || die($DBI::errstr."\n");
  $db->{AutoCommit}=0;
  $db->{RaiseError}=1;

  my $result;
  my $cmd="BEGIN ?:=$proc($placeholders); END;";
  my $stmt=$db->prepare($cmd);
  my $i=1;
  $stmt->bind_param_inout($i, \$result, {ora_type=>SQLT_CHR});
  for my $var (@{$vars}) {
    $stmt->bind_param(++$i, $var);
  };
  $stmt->execute();
  $db->disconnect();
  return $result;
$$ language plperlu;

Esta é uma versão que vale para qualquer número de parâmetros. Escrevi também uma versão para nenhum parâmetro, um parâmetro, e dois parâmetros.

Como essa função faz algumas coisas perigosas, ela só pode ser criada por uma conta administrativa. Além disso, a linguagem escolhida é a plperlu, quando rotinas normais podem ser declaradas com plperl.

Os parâmetros são inseridos como variáveis de ligação (bind variables), então não me preocupei muito com a segurança delas. O nome da rotina, entretanto é inserida diretamente no comando. Então, adicionei uma linha para limpar caracteres indesejados:

  $proc=~s/[^A-Za-z0-9_.]//g;

Essa linha retira todos os caracteres que não forem alfabéticos, numéricos, traço-baixo, ou ponto.

Os dados de conexão são escondidos como variáveis de ambiente do usuário que roda o banco. Além disso, no lado do Oracle, defini um pool de conexões (usando DRCP - Database Resident Connection Pooling). O tempo de execução das chamadas variava entre 300ms e 500ms. Com o DRCP, o teto passou a ser 300ms e normalmente elas são invocadas em cerca de 100ms.

No banco Oracle, adicionei uma entrada ao tnsnames.ora:

DB_POOLED =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = host.com)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = POOLED)
      (SID = DB)
    )
  )

E depois iniciei a pool com um comando:

execute dbms_connection_pool.start_pool();


Posso invocar uma função remota assim:

select call_remote_function('substr', array['postgresql', '2', '3']);

Mas prefiro criar funções locais para esconder a pilantragem:

CREATE OR REPLACE FUNCTION 
oracle_substr(p_str varchar, p_start varchar, p_length varchar) RETURNS varchar AS $$
begin
  return call_remote_function('substr', array[p_str, p_start, p_length]);
end;
$$ LANGUAGE plpgsql;

segunda-feira, 28 de novembro de 2016

Transporte de um Tablespace para outro Diskgroup no Oracle

Um diskgroup de minha base (11g) estava perigosamente cheio, então resolvi transportar um tablespace pouco usado para um diskgroup mais lento.

Suponha que os datafiles do tablespace TSX estejam no diskgroup +DG1 e que serão transportados ao diskgroup +DG2. A tarefa é simples e envolve os seguintes passos para cada datafile (executados no RMAN):
  1. Fazer uma cópia no diskgroup destino: backup as copy datafile file# format '+DG2';
  2. Colocar o datafile atual offline: sql 'alter database datafile file# offline';
  3. Fazer a troca para o backup: switch datafile file# to copy;
  4. Fazer recover do novo datafile: recover datafile file#;
  5. Colocar o datafile online: sql 'alter database datafile file# online';
  6. Apagar datafile original: delete datafilecopy 'filename';
Se o tablespace tiver muitos arquivos, ou a tarefa tiver que ser executada em vários ambientes, vale a pena escrever uma consulta (ou uma função) para gerar o script:

select case step.n
           when 1 then
             'backup as copy datafile '||file#||' format ''+DG2'';'
           when 2 then
             'sql ''alter database datafile '||file#||' offline'';'
           when 3 then
             'switch datafile '||file#||' to copy;'
           when 4 then
             'recover datafile '||file#||';'
           when 5 then
             'sql ''alter database datafile '||file#||' online'';'
           when 6 then
             'delete datafilecopy '''||df.name||''';'
          end
       esac
from v$tablespace ts
     inner join v$datafile df on ts.ts#=df.ts#,
    (select rownum n from all_tables where rownum<7) step
where ts.name=:TABLESPACE_NAME
order by df.file#, step.n
E o resultado será algo assim:

backup as copy datafile 106 format '+DG2';
sql 'alter database datafile 106 offline';
switch datafile 106 to copy;
recover datafile 106;
sql 'alter database datafile 106 online';
delete datafilecopy '+DG1/mydb/datafile/tsx_data.265.122421389';
backup as copy datafile 138 format '+DG2';
sql 'alter database datafile 138 offline';
switch datafile 138 to copy;
recover datafile 138;
sql 'alter database datafile 138 online';
delete datafilecopy '+DG1/mydb/datafile/tsx_data.263.183291830';

quarta-feira, 1 de junho de 2016

Ajustando tablespaces com facilidade

Uma tarefa recorrente na manutenção de um banco de dados Oracle é o de colocar objetos nos tablespaces corretos. Para facilitar o trabalho, recorri ao metadados. A rotina abaixo procura todas as tabelas, todos os índices e todos os LOBs de um schema e verifica se estão nos tablespaces indicados.

create or replace procedure 
  ajustar_tablespaces(p_schema varchar2, 
                      p_table_ts varchar2, 
                      p_index_ts varchar2, 
                      p_lob_ts varchar2) is
begin

  --Indices
  for r in 
   (select 'alter index '||p_schema||'.'|| index_name 
         ||' rebuild tablespace '||p_index_ts cmd 
    from all_indexes
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_index_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --Tabelas
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move tablespace '||p_table_ts cmd 
    from all_tables 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_table_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --LOBs
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move lob('||column_name||') store as (TABLESPACE '||p_lob_ts||')' cmd 
    from all_lobs 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_lob_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;
  
  --Reconstroi os indices
  for r in 
   (select 'alter index '||p_schema||'.'||index_name
         ||' rebuild' cmd  
    from all_indexes 
    where owner=p_schema 
      and status='UNUSABLE') 
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

end;
Então, para corrigir os tablespaces dos objetos de um schema, eu poderia fazer uma chamada como esta:

  exec ajustar_tablespaces('INFO', 'ts_data01', 'ts_index02', 'ts_lobs');

Os comandos são sendo impressos para que se possa acompanhar o andamento do trabalho, principalmente se os objetos forem grandes.

Abaixo, o mesmo código como um bloco a fim de facilitar execuções ad-hoc.

declare
  p_schema varchar2(100):='ABXY';
  p_table_ts varchar2(100):='DTBS1'; 
  p_index_ts varchar2(100):='ITBS1'; 
  p_lob_ts varchar2(100):='LTBS1';
begin

  --Indices
  for r in 
   (select 'alter index '||p_schema||'.'|| index_name 
         ||' rebuild tablespace '||p_index_ts cmd 
    from all_indexes
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_index_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --Tabelas
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move tablespace '||p_table_ts cmd 
    from all_tables 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_table_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --LOBs
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move lob('||column_name||') store as (TABLESPACE '||p_lob_ts||')' cmd 
    from all_lobs 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_lob_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;
  
  --Reconstroi os indices
  for r in 
   (select 'alter index '||p_schema||'.'||index_name
         ||' rebuild' cmd  
    from all_indexes 
    where owner=p_schema 
      and status='UNUSABLE') 
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

end;

sexta-feira, 27 de maio de 2016

Flashback de código no Oracle

Depois de uma tentativas desastrada de parcialmente atualizar uma package, precisei usar o flashback para recuperar o código original. Isso é fácil no Oracle:

  select text 
  from all_source as of timestamp sysdate -1/24 --uma hora
  where owner=:owner and name=:package_name and type='PACKAGE BODY'

Infelizmente, o SQLDeveloper exporta o texto com linhas adicionais. Contornar isso com um pouco de Perl é simples:

  perl -ne "print if !/^$/" package.sql > package_sem_linhas_vazias.sql

Esta linha de Perl imprime todas as linhas do arquivo (último parâmetro) que tiverem alguma coisa (/^$/ é verdadeiro para linhas vazias).

segunda-feira, 2 de junho de 2014

Procurando um valor no Oracle

Uma aplicação de ERP estava gerando um erro e eu precisava encontrar o registro no banco de dados (Oracle). As tabelas têm nomes inescrutáveis, então decidi que eu tinha que pesquisar todas para encontrar o valor que enxergava no monitor.

A solução foi bastante simples; com a ajuda dos metadados, percorri todas as colunas de tipos interessantes a procura do valor.


declare
  s varchar2(4096);
begin
  for i in (select * 
            from all_tab_cols 
            where owner='XY' 
                  and data_type in ('NUMBER','VARCHAR2') 
            order by table_name) 
  loop
    begin
      execute immediate 'select '||i.column_name
                     ||' from xy.'||i.table_name
                     ||' where '||i.column_name||'=''145960''' 
              into s;
      dbms_output.put_line(i.table_name||'.'||i.column_name);
    exception when no_data_found then null;
    end;
  end loop;
end;
Isto é, determinei que no esquema XY apenas interessam as colunas NUMBER e VARCHAR2 e pesquisei coluna por coluna todas as tabelas. Por sorte, XY tem, ao todo, pouco mais que 1 milhão de registros. O valor que eu procurava era 145960, como se pode deduzir da consulta gerada para cada coluna. Se a consulta não retorna nada, o Oracle lança o erro no_data_found. Por isso, o dbms_output.put_line() só é executado quando o valor for encontrado.

Em poucos segundos, encontrei a tabela e a coluna. Devido ao sucesso fácil, não encontrei motivos para transformar a consulta numa procedure ou numa function.

terça-feira, 1 de dezembro de 2009

Paginação de resultados no Oracle

Certa vez deparei-me com um problema aparentemente simples: era preciso gerar um relatório paginado que, ademais, exibisse a cada página o primeiro e o último registros. O sistema era escrito em Java e a interface em JSP. Eu gosto de separar bem as atribuições de cada camada, então não estava disposto a fazer no Java nada além de simplesmente percorrer o ResultSet e exibir os dados.

Após algumas tentativas criei uma consulta com a estrutura da seguinte:

select ub.*, rownum rb from (
 select ua.*, rownum ra from (
  select nome, cpf from usuario
  where nome like 'B%' 
  order by nome asc
 ) ua order by nome desc
) ub
where ra=1 or rownum=1 or ra between 10 and 20
order by nome asc


Nesse caso, a consulta retorna uma lista de usuários (cujos nomes iniciem com B) com seus respectivos CPFs e ordenados por nome. A cláusula where indica que são pesquisados todos os registros entre o décimo e o vigésimo, além do primeiro e o último. O select da consulta é o mais de dentro, os outros apenas fazem uma pequena ginástica para paginar o resultado.

Como a consulta retorna o primeiro e o último registros, é possível colocar no cabeçalho uma indicação como "Registros 10 a 20 (Beatriz a Bianca) de 81 (Bárbara a Bruna)", sem que seja preciso executar nenhuma consulta adicional.

Não é difícil criar uma classe para automaticamente envolver qualquer consulta com os selects externos, mas o melhor mesmo é colocar as consultas dentro de uma procedure. Esse tipo de select, eu descobri, é igual ou melhor a simplesmente contar os registros e depois selecionar os interessantes. Se não está claro o que ele faz, eis um passo-a-passo:

  1. O select interno realiza a consulta;
  2. O select do meio inverte a seleção e agrega o rownum (ra tem a numeração na ordem correta, embora os registros fiquem na ordem invertida);
  3. O select externo inverte novamente a seleção e filtra os registros interessantes.

Essa ginástica toda acontece porque o Oracle atribui o rownum aos registros e depois os ordena. Se não fosse por isso, bastariam dois selects. Apesar de contente com a solução, eu achava que ainda era possível simplificar. Descobri então, as funções analíticas. Não vou explicar o que fazem; vou pular direto para a solução.

select u.*, rownum rb from (
 select nome, cpf,
        row_number() over (order by nome asc) ra
 from usuario 
 where nome like 'B%'
 order by nome desc
) u
where ra=1 or rownum=1 or ra between 10 and 20
order by nome asc

A função row_number() produz um número para cada registro, conforme a ordenação indicada pela cláusula over. O select interno ordena os registros ao contrário, mas o row_number() over (order by nome asc) os conta na ordem direta. O select externo inverte a seleção e agrega um rownum. Como o rownum é atribuido antes da ordenação, o registro com rb=1 é o último (porque é o primeiro registro do select interno). A coluna ra já está na ordem direta, então podemos usá-la para selecionar o primeiro registro e os da página atual (entre 10 e 20).