domingo, fevereiro 25, 2007

Exists e Not Exists

Uma das funções mais úteis em um SGBD é poder
fazer pesquisas excluíndo ou incluíndo certos
elementos (registros) pertecentes a um grupo ou
outro. Isso pode ser feito no MySQL também
usando o EXISTS e o NOT EXISTS existente em ou-
tros inúmeros SGBDS.

Usando a base de dados TESTE e a tabela Contato
que já havíamos criado vamos criar uma tabela
para guardar uma lista de códigos de contatos
indesejados, a lista negra:

CREATE TABLE LISTANEGRA (
CODCONTATO INT NOT NULL
) ENGINE = INNODB;

Agora em contatos vamos inserir dois indesejá-
veis contatos:

INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'Spammer');
INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'Hacker');

Agora como selecionamos somente os contatos que
não estão dentre os indesejáveis?
Usando NOT EXIST:

SELECT co.CodContato, co.NomeContato
FROM Contato co
WHERE NOT EXISTS (SELECT ln.CodContato
FROM ListaNegra ln
WHERE ln.CodContato =
co.CodContato)
ORDER BY co.NomeContato;

+------------+-------------+
| CodContato | NomeContato |
+------------+-------------+
| 1 | Joao |
| 2 | Maria |
+------------+-------------+
2 rows in set (0.00 sec)

Simplesmente estamos trabalhando com operações
de conjuntos.

Agora vamos selecionar o nome dos contatos que
constam entre os barrados:

SELECT co.CodContato, co.NomeContato
FROM Contato co
WHERE EXISTS (SELECT ln.CodContato
FROM ListaNegra ln
WHERE ln.CodContato =
co.CodContato)
ORDER BY co.NomeContato;

+------------+-------------+
| CodContato | NomeContato |
+------------+-------------+
| 4 | Hacker |
| 3 | Spammer |
+------------+-------------+
2 rows in set (0.00 sec)

Aí estão eles.

Bom por hora é isso, ainda estou vendo exemplos
de subqueries mas me parece que o que eu pre-
tendia fazer no MySQL não é válido.

Outra coisa que me deixou meio desmotivado é o
fato de somente o MySQL 5.1.x aceitar o agenda-
mento de tarefas. Por exemplo eu agendar que
um procedimento que gere um script de carga de
dados rode durante a madrugada quando meu sis-
tema estiver ocioso. Essa função somente seria
possível com a versão beta do SGBD.

sexta-feira, fevereiro 23, 2007

Links

Nestes endereços é possível ver os scripts dos assuntos que falamos aqui:

http://br.geocities.com/sadbuttruebr/blog/scripts_
mysql/procedure.txt

http://br.geocities.com/sadbuttruebr/blog/scripts_
mysql/function.txt

http://br.geocities.com/sadbuttruebr/blog/scripts_
mysql/views.txt


Se preferirem depois de descarregar os arquivos podem renomeá-los trocando a extensão de .txt para .sql e executar todos os comandos automáticamente entrando com o comando SOURCE no console:

mysql> source c:\temp\procedure.sql;

Não esqueçam de comentar!!!

quarta-feira, fevereiro 21, 2007

Visões

As visões podem nos dar uma boa solução quando
desejamos ver dados compilados de várias tabe-
las. Um exemplo clássico seria um sistema de
controle de estoque que se poderia ter em uma
visão (view) com os dados do produto, compra,
etc...

Este é um exemplo de como pode ser criado uma
visão no MySQL.

CREATE TABLE PRODUTO (
CODPRODUTO INT NOT NULL,
NOMEPRODUTO VARCHAR(100) NOT NULL,
VALORPRODUTO DECIMAL(19,2) NOT NULL
) ENGINE = INNODB;

ALTER TABLE Produto ADD PRIMARY KEY (CODPRODUTO);

INSERT INTO Produto(CodProduto, NomeProduto, ValorProduto
) VALUES(1, 'Teclado USB PT-BR', 34.90);
INSERT INTO Produto(CodProduto, NomeProduto, ValorProduto
) VALUES(2, 'Monitor 17" LCD', 780.00);
INSERT INTO Produto(CodProduto, NomeProduto, ValorProduto
) VALUES(3, 'Gabinete ATX', 112.80);
INSERT INTO Produto(CodProduto, NomeProduto, ValorProduto
) VALUES(4, 'Papel A4 500 fls', 11.37);

CREATE TABLE VENDA (
CODTRANSACAO INT NOT NULL,
CODUSUARIO INT NOT NULL,
CODPRODUTO INT NOT NULL,
QTDPRODUTO INT NOT NULL
) ENGINE = INNODB;

ALTER TABLE VENDA ADD INDEX (CODUSUARIO);
ALTER TABLE Venda ADD FOREIGN KEY (CODPRODUTO) REFERENCES
Produto(CODPRODUTO) ON DELETE RESTRICT;

INSERT INTO Venda(CodTransacao, CodUsuario, CodProduto,
QtdProduto) VALUES(1, 51, 4, 2);
INSERT INTO Venda(CodTransacao, CodUsuario, CodProduto,
QtdProduto) VALUES(1, 51, 1, 1);

INSERT INTO Venda(CodTransacao, CodUsuario, CodProduto,
QtdProduto) VALUES(2, 171, 2, 1);
INSERT INTO Venda(CodTransacao, CodUsuario, CodProduto,
QtdProduto) VALUES(2, 171, 3, 1);
INSERT INTO Venda(CodTransacao, CodUsuario, CodProduto,
QtdProduto) VALUES(2, 171, 1, 1);

CREATE OR REPLACE VIEW V_VENDA AS
SELECT v.CodTransacao, v.CodUsuario, v.QtdProduto,
p.NomeProduto, v.QtdProduto * p.ValorProduto
TotalItem
FROM Produto p, Venda v
WHERE v.CodProduto = p.CodProduto
ORDER BY v.CodTransacao, p.CodProduto;


Supondo que tivessemos essas tabelas poderiamos
fazer um SELECT * FROM V_Venda e saber todos os
detalhes da venda sem ter que se preocupar em
montar um select gigantesco a cada vez que de-
sejassemos ver os detalhes de uma venda.

Experimente:

SELECT *
FROM V_Venda
GROUP BY CodUsuario;

SELECT *
FROM V_Venda
WHERE CodUsuario = 171
GROUP BY CodUsuario;

SELECT CodUsuario, SUM(TotalItem) TotalVenda
FROM V_VENDA
WHERE CodUsuario = 171
GROUP BY CodUsuario;

Muito útil isso quando não se tem que programar
sobre os registros, ou seja uma visão mesmo do
que está acontecendo. Caso contrário, se fosse
para manusear os dados, eu ao menos preferiria
o fazer em cursores em um procedimento
(procedure). :)

Em breve: Subqueries.

quarta-feira, fevereiro 14, 2007

Funções


Aí está um exemplo da criação e utilização de funções no MySQL.

mysql> DROP FUNCTION IF EXISTS F_NOMEMES;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DELIMITER //

mysql> CREATE FUNCTION F_NOMEMES (P_MES INT) RETURNS
VARCHAR(30)
BEGIN
DECLARE MesExtenso VARCHAR(15);

CASE P_Mes
WHEN 01 THEN
SET MesExtenso = 'Janeiro';
WHEN 02 THEN
SET MesExtenso = 'Fevereiro';
WHEN 03 THEN
SET MesExtenso = 'Março';
WHEN 04 THEN
SET MesExtenso = 'Abril';
WHEN 05 THEN
SET MesExtenso = 'Maio';
WHEN 06 THEN
SET MesExtenso = 'Julho';
WHEN 07 THEN
SET MesExtenso = 'Julho';
WHEN 08 THEN
SET MesExtenso = 'Agosto';
WHEN 09 THEN
SET MesExtenso = 'Setembro';
WHEN 10 THEN
SET MesExtenso = 'Outubro';
WHEN 11 THEN
SET MesExtenso = 'Novembro';
WHEN 12 THEN
SET MesExtenso = 'Dezembro';
ELSE
SET MesExtenso = 'Mês inválido!';
END CASE;

RETURN MesExtenso;

END;
//

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DELIMITER ;

mysql> SELECT F_NOMEMES(DATE_FORMAT(NOW(), '%m')) Mes_Atual;

Saída no console:
+-----------+
| Mes_Atual |
+-----------+
| Fevereiro |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT F_NOMEMES(12) Mes_Atual;

Saída no console:
+-----------+
| Mes_Atual |
+-----------+
| Dezembro |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT F_NOMEMES(13) Mes_Atual;

Saída no console:
+---------------+
| Mes_Atual |
+---------------+
| Mês inválido! |
+---------------+
1 row in set (0.00 sec)

Não há muito o que falar sobre funções então me despeço por
aqui.

Em breve: visões.

quarta-feira, fevereiro 07, 2007

Raise trigger

As vezes acho que esse MySQL não pode estar
sendo usado para valer por empresas reais, do
mundo real, etc... ou eu que sou paranôico e
gosto de fazer uma grande parte da lógica dos
meus sistemas no banco de dados dividindo essa
responsabilidade com a aplicação. Não sei se
isso é certo, bom tema para uma discussão, quem
quizer pode postar aí o que acha. ;)

Bom estava eu aqui feliz da vida quando me de-
parei com um fato lamentável... no MySQL não
tenho como "matar" um gatilho, procedimento,
etc... Não há uma instrução semelhante a
RAISE_APPLICATION_ERROR do Oracle.

Caramba, e agora? Criar um procedimento que
trate os erros da aplicação? Insano! Não vou
escrever isso, isso tem que ser feito pelos de-
senvolvedores do SGBD e não por mim (po que má
vontade a minha :P)!

Mas a idéia até que por hora, apenas por hora,
soa como uma gambiarra (um bacalhau como eu
prefiro dizer) razoável. Mas mesmo assim seria
um trabalho insano, onde TODO o controle da
transação estaria na minha mão, e eu nem sei se
isso é possível!

Seria mais ou menos isso:

CREATE PROCEDURE x (INT parametro1 INT, OUT
resultado INT, OUT sqlError)
BEGIN

-- Digamos que eu tenha um select aqui dentro
-- e que o mesmo não possa vir 0 ou nulo em
-- um determinado campo.

-- Eu percorreria meu cursor e a cada linha
-- testaria se há valor no campo, se não ti-
-- ver eu setor meu sqlError com uma mensagem
-- de erro qualquer e pararia o loop.

-- Na aplicação eu teria que controlar se ele
-- é nulo, se for faz alguma coisa ou até
-- mesmo manda o banco dar commit\rollback no
-- procedimento e então poderia chamar uma
-- p_erros(IN mensagem, OUT erro); Que retor-
-- naria uma mensagem baseada em um código,
--etc...

END;

Fiquei cansado só de pensar em implementar isso
tudo.

Fica aqui um exemplo e como trabalhar com pro-
cedimentos e cursores.

Usando a base de dados TESTE que criamos:

mysql> DROP TABLE IF EXISTS Recado;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DROP TABLE IF EXISTS Contato;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DROP PROCEDURE IF EXISTS
P_RECADOSPORCONTATO;

Saída no console: Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TABLE CONTATO(
CODCONTATO INT NOT NULL,
NOMECONTATO VARCHAR(100) NOT NULL
) ENGINE = INNODB;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> ALTER TABLE CONTATO ADD PRIMARY KEY (CODCONTATO);

Saída no console: Query OK, 0 rows affected
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE CONTATO ADD INDEX (CODCONTATO);

Saída no console: Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> DELIMITER //

mysql> CREATE TRIGGER BIF_CONTATO BEFORE INSERT ON CONTATO
FOR EACH ROW

BEGIN
DECLARE novoCodigo INT;

SELECT IFNULL(MAX(CodContato), 0) + 1
INTO novoCodigo
FROM Contato;

SET NEW.CodContato = novoCodigo;

END;
//

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TRIGGER BUF_CONTATO BEFORE UPDATE ON CONTATO
FOR EACH ROW

BEGIN

SET NEW.CodContato = OLD.CodContato;

END;
//

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'Joao');

Saída no console: Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'Maria');

Saída no console: Query OK, 1 row affected (0.00 sec)

mysql> CREATE TABLE RECADO(
CODCONTATO INT NOT NULL,
RECADO VARCHAR(500) NOT NULL,
CODREMETENTE INT NOT NULL
) ENGINE = INNODB;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> ALTER TABLE RECADO ADD INDEX (CODCONTATO);

Saída no console: Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE RECADO ADD FOREIGN KEY (CODCONTATO) REFERENCES CONTATO(CODCONTATO) ON DELETE CASCADE;

Saída no console: Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE RECADO ADD FOREIGN KEY (CODREMETENTE) REFERENCES CONTATO(CODCONTATO) ON DELETE CASCADE;

Saída no console: Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> INSERT INTO Recado(CodContato, Recado, CodRemetente) VALUES(2, 'Nao esquece do happy hour hoje!', 1);

Saída no console: Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO Recado(CodContato, Recado, CodRemetente) VALUES(1, 'Ok, irei sem falta.', 2);
Saída no console: Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO Recado(CodContato, Recado, CodRemetente) VALUES(2, 'É lá no buteco do Joaquin.', 1);
Saída no console: Query OK, 1 row affected (0.02 sec)

mysql> DELIMITER //

mysql> CREATE PROCEDURE P_RECADOSPORCONTATO(IN CodContato INT
, OUT total INT)

BEGIN
-- Variável que controla o loop
DECLARE linhaFinal INT DEFAULT 0;
-- Recebe o total de recados do contato
DECLARE qtdRecados INT DEFAULT 0;

DECLARE csRecados CURSOR FOR
SELECT COUNT(*)
FROM Recado re
WHERE re.CodContato = CodContato;

-- O controlador do fluxo
DECLARE CONTINUE HANDLER FOR NOT FOUND SET
linhaFinal = 1;


OPEN csRecados; -- Abre o cursor

-- Começa a varedura nos registros
looping: LOOP

-- Coloca na variável os dados da linha atual
FETCH csRecados INTO qtdRecados;

-- Controla para saber se tem que deixar o loop
IF linhaFinal = 1 THEN
LEAVE looping;
END IF;

SET total = qtdRecados; -- Seta uma variável
-- (neste caso um paâmetro)
-- com o resultado do
-- calculo da linha

END LOOP looping; -- Fecha o loop

END;
//

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DELIMITER ;

mysql> CALL P_RECADOSPORCONTATO(2, @resultado);

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @resultado;

Saída no console:
+------------+
| @resultado |
+------------+
| 2 |
+------------+
1 row in set (0.00 sec)

Em breve: funções.

quinta-feira, fevereiro 01, 2007

Iniciando com MySQL

Comecei a desenvolver um projeto para uma aca-
demia onde pratico jiu-jitsu. Bom o "projeti-
nho" não é revolucionário, é só uma forma de
cadastro de alunos e controle de pagamento das
mensalidades.
A linguagem usada eu ainda vou decidir, prova-
velmente ficará entre ASP e JSP a disputa :)
mas a base de dados eu já decidi que ficará em
um SGBD MySQL e o sistema rodará sobre Windows.


Porque MySQL?

Não vejo motivos para não usá-lo na verdade.
O único problema que eu encontrei na versão que
estou usando (5.0.24) é o não suporte a CHECK
CONSTRANTS, de resto ele suporta tudo e é ex-
tremamente rápido, ainda mais para um sistema
com as características deste que será 90% uti-
lizado para consultas e inserções de dados.


Iniciando.

Bom eu venho da escola Oracle (e para mim não
existe SGBD que o bata) então tive que dar uma
pesquisada em como fazer algumas coisas e tal
e é isso que comprartilharei aqui no blog com
quem mais precisar.


Instalando.

Não vou entrar nesse ponto, é como instalar
qualquer coisa no Windows - NEXT, NEXT, NEXT -
o único ponto a ser visto é definir uma senha
segura para o usuário root.


Nosso ponto de partida.

O que precisamos depois de instalar o SGBD pa-
ra podermos começar? Uma base de dados!
Vamos criar uma base de dados então. O MySQL já
vem com uma base de dados de testes chamada
test, mas como o objetivo aqui é começar a fa-
zer tudo que fariamos se fossemos começar a
desenvolver nossos sistemas... lá vamos nós:

Lembra da senha do root? Então em um console
vá para a pasta de instalação do MySQL e entre
no diretório bin. Agora execute:

shell> mysql -u root -p
Enter password: senha123

Saída no console:
Welcome to the MySQL monitor. Commands end
with ; or \g.
Your MySQL connection id is 1 to server
version: 5.0.24a-community-nt

Type 'help;' or '\h' for help. Type '\c' to
clear the buffer.

Para criar uma base de dados:

mysql> CREATE DATABASE TESTE;

Saída no console: Query OK, 1 row affected (0.00 sec)


Alterando a base de dados que estamos usando.

Agora vamos usar a base de dados:

mysql> USE TESTE
Saída no console: Database changed


Criando uma tabela.

De nada adianta criarmos uma base se não tiver-
mos alguma(s) tabela(s) para brincarmos. Vamos
fazer um teste com chaves primárias, estrangei-
ras, procedimentos, selects com join, alterar
tabelas, criar gatilhos, etc... um exemplo um
pouco mais completo para um suposto sistema de
envio de mensagens.

1) Criar a tabela CONTATO:

mysql> CREATE TABLE CONTATO(
CODCONTATO INT NOT NULL,
NOMECONTATO VARCHAR(100) NOT NULL
) ENGINE = INNODB;

Saída no console: Query OK, 0 rows affected (0.02 sec)

INNODB é uma engine que suporta chaves e outras
funções de bancos integros.

2) Alterar a tabela adicionando uma chave pri-
mária e um índice a tabela:

mysql> ALTER TABLE CONTATO ADD PRIMARY KEY (CODCONTATO);

Saída no console: Query OK, 0 rows affected (0.44 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE CONTATO ADD INDEX (CODCONTATO);

Saída no console: Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0

Nossos contatos devem ter código seqüencial
calculado pelo sistema, nada melhor do que cri-
armos uma trigger neste ponto.

3) Criar o gatilho de inserção:

mysql> DELIMITER //

mysql> CREATE TRIGGER BIF_CONTATO BEFORE INSERT ON CONTATO
FOR EACH ROW

BEGIN

DECLARE novoCodigo INT;

SELECT IFNULL(MAX(CodContato), 0) + 1
INTO novoCodigo
FROM Contato;

SET NEW.CodContato = novoCodigo;

END;
//

Saída no console: Query OK, 0 rows affected (0.01 sec)

mysql> DELIMITER ;

A mudança de delimitador de linha serve para
que o MySQL não interprete o ';' nos comandos
que listamos dentro de um gatilho, procedimen-
to, etc...

4) Criar o gatilho de atualização:

Não queremos que uma pessoa consiga atualizar
o código do usuário pois esse é a nossa chave
primária sobre o registro, então podemos fazer
um gatilho que dispara a cada atualização na
tabela atribuindo ao registro sempre o seu an-
tigo código.

mysql> DELIMITER //

mysql> CREATE TRIGGER BUF_CONTATO BEFORE UPDATE ON CONTATO
FOR EACH ROW

BEGIN

SET NEW.CodContato = OLD.CodContato;

END;
//

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> DELIMITER ;


Inserindo dados e testando a integridade da
base.

Vamos inserir alguns registros na tabela CONTATO:

mysql> INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'João');

Saída no console: Query OK, 1 row affected
(0.00 sec)

mysql> INSERT INTO Contato(CODCONTATO, NOMECONTATO) VALUES(0, 'Maria');

Saída no console: Query OK, 1 row affected (0.00 sec)

Vamos listar nossos contatos cadastrados:

mysql> SELECT CodContato, NomeContato
FROM Contato;

Saída no console:
+------------+-------------+
| CodContato | NomeContato |
+------------+-------------+
| 1 | João |
| 2 | Maria |
+------------+-------------+
2 rows in set (0.00 sec)

Se tentarmos atualizar o código de qualquer um
dos contatos o SGBD não dará mensagem de erro
mas sim não alterará o registro pois o nosso
gatilho de atualização não permite.

Bom agora sem mais rodeios vamos criar uma ta-
bela de recados:

mysql> CREATE TABLE RECADO(
CODCONTATO INT NOT NULL,
RECADO VARCHAR(500) NOT NULL,
CODREMETENTE INT NOT NULL
) ENGINE = INNODB;

Saída no console: Query OK, 0 rows affected (0.00 sec)

mysql> ALTER TABLE RECADO ADD INDEX (CODCONTATO);

Saída no console: Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE RECADO ADD FOREIGN KEY (CODCONTATO) REFERENCES CONTATO(CODCONTATO) ON DELETE CASCADE;

Saída no console: Query OK, 0 rows affected
(0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> ALTER TABLE RECADO ADD FOREIGN KEY (CODREMETENTE) REFERENCES CONTATO(CODCONTATO) ON DELETE CASCADE;

Saída no console: Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0

Notem que temos uma chave estrangeira de uma tabela para outra aqui, ou seja, um recado pertence a um cotato. Tente excluir um contato e todas as suas mensagens vão ser apagadas também.

E vamos inserir dois recados de teste:

mysql> INSERT INTO Recado(CodContato, Recado, CodRemetente) VALUES(2, 'Nao esquece do happy hour hoje!', 1);

Saída no console: Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO Recado(CodContato, Recado, CodRemetente) VALUES(1, 'Ok, irei sem falta.', 2);

Saída no console: Query OK, 1 row affected (0.02 sec)

Ok, agora selecionando os dados de maneira mais
eficaz para podermos ver o que foi mandado.
Nesta instrução relacionaremos a tabela contato
2 vezes para pegar o nome do contato de desti-
natário e remetente do recado:

mysql> SELECT co1.NomeContato Para, rec.Recado,
co2.NomeContato De
FROM Contato co2, Contato co1, Recado rec
WHERE rec.CodContato = co1.CodContato
AND rec.CodRemetente = co2.CodContato;

Saída no console:
+-------+-----------------------------+-------+
| Para | Recado | De |
+-------+-----------------------------+-------+
| Maria | Nao esquece do happy hour | Joao |
| | hoje! | |
| Joao | Ok, irei sem falta. | Maria |
+-------+-----------------------------+-------+
2 rows in set (0.00 sec)

Bom esse é um exemplo bem completo de uso do
MySQL. Agora me resta ir descobrindo o resto e
modelando o sistema.

Em breve: funções e procedimentos.

Leu, gostou, não gostou, quer elogiar ou criti-
car? Comente o post!