Eu sempre achei um fenômeno sobrenatural a magia utilizada nas querys SQL quando envolviam Selects dentro de Selects. Achei muito engraçado quando recebi o e-mail do Thiago, estagiário aqui da empresa, sobre uma query que eu havia feito e enviado pra ele:
Esse seu método é meio mágico, parece o tipo de coisa que “não deveria funcionar mas funciona” quando vc olha! Não pq não faz sentido (pq faz), mas pq esses bancos de dados implicam com coisas bem mais simples que isso… Enfim, funciona, mesmo qdo não é para buscar por um codigo específico (como você fazia antes), o que é ótimo. (Thiago)
O mais engraçado disso tudo é que eu também nunca parei pra pensar a fundo como isso funcionava, simplesmente utilizava. Sim, usava como mágica mesmo, mas agora decidi estudar a fundo para saber como isso resolve os meus problemas e decidi compartilhar aqui com vocês.
Atenção: Se você não conhece nada de informática, nunca ouviu falar em SQL, query e outros termos nesse artigo, pode parar de ler aqui porque o negócio é muito técnico e chato! Rsrsr Eu, por exemplo, não conseguiria ler o segundo parágrafo de um texto que falasse sobre o último paredão do BBB.
Para começar, vou mostrar logo a solução. Abaixo está a query SQL que, para nós atualmente, é composta por pura magia:
1: SELECT
2: a1.*
3: FROM
4: Acao a1
5: WHERE
6: a1.situacaoAcao <> 'E' AND
7: a1.dtAlteracaoAcao = (
8: SELECT
9: MAX(a2.dtAlteracaoAcao)
10: FROM
11: Acao a2
12: WHERE
13: a2.indAcaoOriginal = a1.indAcaoOriginal
14: )
O que ela faz? Retorna todas as Ações não excluídas e somente a mais atual. Simples né? Sim, pois é exatamente o que eu queria.
Para entender melhor, vamos ver a estrutura da tabela Ação:
Ação |
---|
indAcao: int IDENTITY(1,1) |
nomeAcao: varchar(200) NOT NULL |
dtAlteracaoAcao: datetime NOT NULL |
situacaoAcao: char(2) NOT NULL |
indAcaoOriginal: int NULL |
*O ponto a observer nessa tabela, é o auto-relacionamento entre o campo indAcaoOriginal com o indAcao. Uma ação nunca é atualizada. Se for necessário alguma atualização, é criado um novo registro, com as novas informações, mas com o indAcaoOriginal apontando para a primeira Ação que foi criada. Assim, nós temos um histórico das atualizações das Ações.
Sabendo o que temos no banco, um monte de Ações individuais (indAcao) auto-relacionadas (indAcaoOriginal) com a Ação original, o desafio era trazer todas as Ações mais recentes, ou seja, com a última data de alteração.
Desmascarando a Mágica
Vamos utilizar a tabela abaixo como exemplo de registros. Ela é o retorno da seguinte query:
1: SELECT
2: a1.*
3: FROM
4: Acao a1
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
1 | Reservar | 16/03/2006 10:10 | AA | 1 |
2 | Reservar Produto | 17/03/2006 11:11 | A | 1 |
3 | Reservar Produtos | 17/03/2006 18:03 | AA | 1 |
4 | Apontar Dados | 18/03/2006 10:10 | AA | 2 |
5 | Apontar Dados | 18/03/2006 11:40 | A | 2 |
6 | Cadastrar Estatística | 18/03/2006 18:00 | AA | 6 |
7 | Cadastrar Funcionário | 18/03/2006 18:20 | AA | 7 |
8 | Revelar Negativos | 19/03/2006 09:00 | AA | 8 |
9 | Verificar Estoque | 19/03/2006 10:05 | AA | 9 |
10 | Verificar Estoque | 20/03/2006 10:10 | A | 9 |
11 | Verificar Estoque | 20/03/2006 13:00 | E | 9 |
12 | Reduzir | 20/03/2006 14:15 | AA | 12 |
13 | Reduzir Custos | 20/03/2006 15:30 | A | 12 |
14 | Reduzir Custos | 20/03/2006 16:45 | AE | 12 |
15 | Demitir Funcionários | 21/03/2006 20:33 | AA | 15 |
16 | Rever Orçamento | 22/03/2006 08:17 | AA | 16 |
17 | Cancelar Contrato | 22/03/2006 09:05 | AA | 17 |
18 | Vender Produto | 22/03/2006 14:18 | AA | 18 |
19 | Vender Produto | 22/03/2006 17:22 | A | 18 |
Filtrando pelos registros excluídos, temos o resultado abaixo:
1: SELECT
2: a1.*
3: FROM
4: Acao a1
5: WHERE
6: a1.situacaoAcao <> 'E'
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
1 | Reservar | 16/03/2006 10:10 | AA | 1 |
2 | Reservar Produto | 17/03/2006 11:11 | A | 1 |
3 | Reservar Produtos | 17/03/2006 18:03 | AA | 1 |
4 | Apontar Dados | 18/03/2006 10:10 | AA | 2 |
5 | Apontar Dados | 18/03/2006 11:40 | A | 2 |
6 | Cadastrar Estatística | 18/03/2006 18:00 | AA | 6 |
7 | Cadastrar Funcionário | 18/03/2006 18:20 | AA | 7 |
8 | Revelar Negativos | 19/03/2006 09:00 | AA | 8 |
9 | Verificar Estoque | 19/03/2006 10:05 | AA | 9 |
10 | Verificar Estoque | 20/03/2006 10:10 | A | 9 |
12 | Reduzir | 20/03/2006 14:15 | AA | 12 |
13 | Reduzir Custos | 20/03/2006 15:30 | A | 12 |
14 | Reduzir Custos | 20/03/2006 16:45 | AE | 12 |
15 | Demitir Funcionários | 21/03/2006 20:33 | AA | 15 |
16 | Rever Orçamento | 22/03/2006 08:17 | AA | 16 |
17 | Cancelar Contrato | 22/03/2006 09:05 | AA | 17 |
18 | Vender Produto | 22/03/2006 14:18 | AA | 18 |
19 | Vender Produto | 22/03/2006 17:22 | A | 18 |
*Somente a Ação de indAcao 11 foi removida.
Se executar a query abaixo de forma isolada, temos o seguinte resultado:
1: SELECT
2: MAX(a2.dtAlteracaoAcao)
3: FROM
4: Acao a2
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
19 | Vender Produto | 22/03/2006 17:22 | A | 18 |
*A função MAX() retorna o maior valor de um campo em um conjunto, que neste caso isolado, o conjunto corresponde a todos os registros da tabela Ação. Ele retorna o registro 19 porque possui a maior data.
Então, o que preciso fazer para resgatar a maior data do conjunto de Ações Originais? Preciso gerar um conjunto separado, contendo todas as Ações Originais, para resgatar essa maior data. Isso é óbvio. Que bom que sabemos definir o que queremos. O problema é conversar com o SQL pra ele entender isso. :)
Vamos executar um chinês, passo-a-passo, para verificarmos como a query mágica funciona realmente.
Passo-a-Passo
A primeira coisa que a query abaixo faz, é resgatar o primeiro registro diferente de excluído da tabela a1.
1: SELECT
2: a1.*
3: FROM
4: Acao a1
5: WHERE
6: a1.situacaoAcao <> 'E' AND
7: a1.dtAlteracaoAcao = (
8: SELECT
9: MAX(a2.dtAlteracaoAcao)
10: FROM
11: Acao a2
12: WHERE
13: a2.indAcaoOriginal = a1.indAcaoOriginal
14: )
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
1 | Reservar | 16/03/2006 10:10 | AA | 1 |
Tendo esse registro, a query interna abaixo, cria o seguinte subconjunto:
1: SELECT
2: MAX(a2.dtAlteracaoAcao)
3: FROM
4: Acao a2
5: WHERE
6: a2.indAcaoOriginal = a1.indAcaoOriginal
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
1 | Reservar | 16/03/2006 10:10 | AA | 1 |
2 | Reservar Produto | 17/03/2006 11:11 | A | 1 |
3 | Reservar Produtos | 17/03/2006 18:03 | AA | 1 |
Ela resgata todos os registros da tabela Ação (a2) que possuem o a2.indAcaoOriginal igual ao a1.indAcaoOriginal do registro atual da tabela Ação (a1). Qual o valor em a1.indAcaoOriginal do primeiro registro resgatado pelos Select externo? O valor é 1, então, retorna todos os registros em a2 que possuam o indAcaoOriginal igual a 1. Simples assim.
Tendo esse subconjunto (indAcaoOriginal = 1), o select interno retorna, através do MAX, a maior data cadastrada, que neste caso, é 17/03/2006 18:03.
Agora, sabendo que a maior data é a 17/03/2006 18:03, o select externo (a1), compara com a data de seu registro.
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
1 | Reservar | 16/03/2006 10:10 | AA | 1 |
Como podemos verificar, a data retornada no MAX não é igual a data do nosso primeiro registro, ou seja, o select irá ignorar essa linha, não exibindo-a, e passará para o segundo registro:
indAcao | nomeAcao | dtAlteracaoAcao | situacaoAcao | indAcaoOriginal |
---|---|---|---|---|
2 | Reservar Produto | 17/03/2006 11:11 | A | 1 |
Pronto, aqui ele irá fazer os mesmos passos explicados anteriormente e verificará que a data continua sendo diferente da maior data do subconjunto das Ações Originais.
E assim, o select percorrerá toda a tabela, retornando somente os registros que possuírem a maior data.
Fácil né? Estou me sentindo o Mr. M, ao revelar a mágica envolvida nessa query. :)
E aí Thiago, entendeu?
Vou propor um desafio em SQL Server 2000. Consulta.
Determinados funcionários recebeu 1/3 de féria em Fevereiro e recebeu abono família em Janeiro, mas não receberam abono família em Fevereiro.
Legal, mas eu costumo utilizar o IN que dá no mesmo! =D
Sei que o artigo é antigo, mas tive um grande problema pra fazer uma procedure que me retornasse a ultima data de um grupo de registros. Parabéns pelo artigo e obrigado, me ajudou a resolver o problema e entender como funciona! =)
Abraço.
Tabela R1
A B C
1 a w
5 c x
1 c w
3 c p
1 d w
2 b y
2 c z
4 c q
3 b p
2 a y
5 a y
4 a q
3 a p
5 b x
1 b w
Tabela R2
B
a
b
c
Obs: as letras maiúsculas "ABC" são a coluna(campo) da tabela R1 e B é a coluna(campo) da Tabela R2. Exemplos: 1º linha de R1 é ABC= 1aw e de R2 é B=a
Quem pode fazer a mesma explicação para as tabelas R1 e R2:
R1 A B C R2 B
1 a w a
5 c x b
1 c w c
3 c p
1 d w
2 b y
2 c z
4 c q
3 b p
2 a y
5 a y
4 a q
3 a p
5 b x
1 b w
SELECT DISTINCT A
FROM R1 AS a
WHERE NOT EXISTS (SELECT * FROM R2 AS b
WHERE NOT EXISTS (SELECT * FROM R1 AS c
WHERE c.A = a.A
AND c.C = a.C
AND c.B = b.B));
Saída:
A
1
3
Informação nunca é demais! Aposto com vocês que a maioria das pessoas que programam hoje em dia fazem uso de códigos SQL como esse acima sem saber como eles funcionam de verdade. O Bruno mandou muito bem na explicação dele. Cheguei até aqui procurando sobre esse assunto de um SELECT dentro do outro. Apesar de já conhecer o método acima, fiquei ainda mais por dentro de como ela funciona. Parabéns!
Meu velho, sem trocadilhos, pois o velho aqui sou eu. Tenho 52 anos. Estava aposentado depois de trabalhar 12 anos gerenciando um departamento de informática (exercitava somente conhecimentos gerenciais) Fiz alguns cursos de lógica, programação, administração de BD … e recentemente voltei ao mercado de trabalho – desenvolvendo algumas aplicações, que utilizam subqueryes. O teu artigo trouxe a ajuda que precisava para entender aquilo que eu fazia intuitivamente e poder explicar com mais propriedade ao demais que estão nesta tarefa.
Parabéns!
Itamar
Tudo bem, não se preocupe. Os estagiários acabam sacaneados de uma forma ou de outra… ¬_¬!
Eu não ligo pra isso não, só quis esclarecer um pouco a minha posição nessa história, que ficou meio vaga…
Tô doido pra acabar esse sistema tb!
Thiago:
Não foi minha intenção lhe chamar de idiota, tanto que eu explico lá no começo que eu também não entendia direito como isso funcionava, que resolvi estudar a coisa toda passo-a-passo pra poder entender. O engraçado de tudo, foi a frase que você escreveu, que como está fora de um contexto propositado, torna-se engraçada.
Não quis chamar ninguém de idiota não.
AH! Eu não tenho nenhuma implicância com estagiários ehn! Alex que gostava de tratar mau! rsrsrsrs
Fica aqui o esclarecimento e um pedido de desculpas pela impressão errada interpretada pelos leitores.
Abraço e vamos terminar esse sistema logo que o negócio tá ficando bom! :)
vlw
Po, falando assim parece até que eu sou idiota! É claro que eu entendo a razão de isso funcionar, simplesmente estranhei o SQL Server ser inteligente ao ponto de rodar a subquery diversas vezes (uma para cada indAcaoOriginal), já que vejo ele implicando com coisas teoricamente mais simples.
Alex:
O que eu escrevi realmente fica sem nexo assim, fora de contexto. O negócio é que eu estava fazendo uma VIEW que retornasse apenas os registros mais novos não excluÃdos de todas as ações, e estava usando um método com GROUP BY. Então o Bruno me mostrou um outro método, com o código parecido com esse aà de cima.
Resolvi tentar com esse método, mas percebi que na query dele ele filtrava o resultado para uma ação especÃfica (a1.indAcaoOriginal = XX no primeiro WHERE), e fiquei surpreso quando o código funcionou para todas elas, significando que a subquery foi executada diversas vezes, buscando a maior data PARA CADA GRUPO DE AÇÕES, coisa que imaginei que não faria, por isso chamei de "mágica".
Vocês e os estagiários, heim… vou te contar! Parece que a gente é anta! Bom, alguns podem até ser, mas asseguro que eu não! =)
Fala Alex!
Viu, quando eu era seu estagiário você não me tratava assim. Falava só: Se vira. KKKKKKKKKKKKKKKKKKKK
Quanto ao explicar a fundo, poderia ter feito uma animação do byte trafegando pelas ponte sul e norte e sendo processados na CPU, armazenados nos registradores e sendo transformados em bits e coisa e tal, mas achei que isso iria esticar um pouquinho a explicação… rsrsrsrs
Quanto ao que o Thiago escreveu, creio que terei de estudar a fundo o que ele disse e escrever um outro artigo, mas creio que esse será muito mais complexo! rsrsrs
Valeu pela visita!
*E só nerd mesmo pra entrar aqui e ler isso… rsrs
Abraço!
Não acredito que vc fez isso tudo por causa de um estagiário!!!! Bem, já que escreveu, achei interessante. Só que quando vc disse que iria se aprofundar no assunto, pensei que fosse explicar o funcionamento do SGBD na hora que recebe uma query. AHUhaUhAuhauhaUHuaHuahUahua
ps. eu li o que o Thiago escreveu umas três vezes e até agora não entendi. Acho que deverÃamos reativar o Lado B!
Alam:
KKKKKKKKKKKKKK
Foi pra ensinar o estagiário não. Foi pra entender mais profundamente mesmo! rsrsrs
Mas agora ele com certeza vai entender. rsrsrs
Valeu pelo comentário!
Ninja:
rsrsr Eu articulista da SQL Magazine? Iria articular vários causos mesmo, ou então, escrever umas matérias de SQL Avançado para Beginners! rsrsrs
Valeu o comentário!
[]s
Mandou muito bem novamente!!!
Vou falar com um amigo meu pra colocar seu artigo na SQL Magazine, vc será o mais novo articulista da revista…
Se o estagiário não sabe o SQL, manda ele fazer um curso?!?!?! rs,rs,rs
Tudo isso pra ensinar ao Estagiário SQL Query?