Construyendo una BD
Veremos ahora como crear una BD con SQL (Structured Query Language). El tipo de SQL que se usa para crear tablas o definir estructuralmente una BD le llamamos DDL (Data Definition Language). Son un set de comandos diferentes a los que usamos para consultar datos.
Creando la BD de Ingresos Hospitalarios
Supongamos el siguiente modelo ER:
Vamos a crearlo con comandos DDL de SQL:
create table
y alter table
El comando create table
, como su nombre lo dice, nos ayuda a crear una tabla.
A continuación, el comando para crear la tabla doctor
:
--
-- TABLE: doctor
--
create table doctor (
id_doctor numeric(4,0) constraint pk_doctor primary key,
nombres varchar(50) NOT NULL ,
apellidos varchar(50) NOT NULL ,
fecha_contratacion date NOT NULL ,
sueldo numeric(8,2) NOT NULL ,
id_especializacion numeric(4) REFERENCES especializacion (id_especializacion)
);
--
CREATE SEQUENCE doctor_id_doctor_seq START 1 INCREMENT 1 ;
ALTER TABLE doctor ALTER COLUMN id_doctor SET DEFAULT nextval('doctor_id_doctor_seq');
--
Vamos a desmenuzar este comando línea por línea:
create table doctor (
: crea la tabladoctor
. Ojo que el PostgreSQL todos los comandos son case-insensitive.id_doctor numeric(4) constraint pk_doctor primary key,
: asigna el 1er atributo, llamadoid_doctor
, de tiponumeric
, de máximo(4,
posiciones, sin punto decimal0)
, y le asigna un constraint de tipoprimary key
, lo cual en automático asigna las restricciones de 1) no poder tener valores repetidos, 2) no poder ser nulo. Adicionalmente, le crea un índice (lo veremos más delante). Por buena práctica, las llaves primarias llevan la sintaxisid_[tabla]
.nombres varchar(50) NOT NULL ,
: creamos el atributonombres
de tipovarchar
(i.e. variable-length character string, osea, string) con(50)
posiciones de longitud máxima y con una restricciónNOT NULL
para evitar registros con esta columna vacía. La columna o atributoapellidos
sigue la misma estructura.- 👀OJO!👀 - si no ponemos
not null
ninull
al especificar la columna, el comportamiento de default es como si pusiéramosnull
, es decir, que la columna (o la relación, si estamos especificando eso) acepte valores nulos
- 👀OJO!👀 - si no ponemos
fecha_contratacion date NOT NULL,
: columna o atributo de tipodate
.sueldo numeric(8,2) NOT NULL,
: columna o atributosueldo
de(8,
posiciones, de las cuales2)
son decimales, y con un constraint de tipoNOT NULL
.id_especializacion numeric(4) REFERENCES especializacion (id_especializacion)
: campo, atributo o columna de tiponumeric
de(4)
posiciones que representará una relación de 1 a 1 con la tablaespecializacion
. Esta relación está dada por el argumentoREFERENCES [tabla] ([llave primaria de tabla a relacionar])
, la cual en automático impone un constraint de tipoforeign key
. A partir de este momento será imposible crear un registro de undoctor
sin asignarle forzosamente una especialidad que ya exista en la tablaespecializacion
.);
: con esto terminan los comandos SQL en PostgreSQL siempre, y el paréntesis cierra el comandocreate table
, pero aún no terminamos de definir la tabla.--
: separador de línea o comentario. Las líneas que comiencen con--
no serán procesadas por el compilador de SQL.CREATE SEQUENCE doctor_id_doctor_seq START 1 INCREMENT 1 ;
: Esta línea crea una secuencia, un objeto de PostgreSQL que representa una serie de números consecutivos y que nos permite implementar la buena práctica de que las llaves primarias sean un entero secuencial. Esta secuencia la llamaremosdoctor_id_doctor_seq
, y su comienzo está definido en 1 por el argumentoSTART 1
y su incremento también en 1 por el argumentoINCREMENT 1
. La buena práctica sugiere que los nombres de las secuencias sea[tabla]_[campo de llave primaria]_seq
.ALTER TABLE doctor ALTER COLUMN id_doctor SET DEFAULT nextval('doctor_id_doctor_seq');
: similar a la línea anterior, ésta nos ayuda a definir un valor por default para nuevos registros mediante el argumentoSET DEFAULT
. Dicho valor por default es una llamada a la funciónnextval
, que obtiene el siguiente valor de una secuencia, en este caso, la que creamos en [9] con nombredoctor_id_doctor_seq
.
Intentemos correr estos comandos en DBeaver.
Dado que estamos creando una tabla que tiene un constraint de llave foránea con la tabla especializacion
, y dado que ésta aún no se encuentra creada, PostgreSQL arrojará un error. La forma de tratarlo es creando primero las tablas que no tengan relaciones (que usualmente están en la periferia del problem domain), y poco a poco ir creando más hasta dejar al final la tabla con mayor número de relaciones.
Vamos a crear la tabla especializacion
para que la creación de la tabla doctor
funcione:
create table especializacion(
id_especializacion numeric(4) constraint pk_especializacion primary key,
nombre varchar(250) not null
);
create sequence especializacion_id_especializacion_seq start 1 increment 1;
alter table especializacion alter column id_especializacion set default nextval('especializacion_id_especializacion_seq')
Después de esto ya podremos ejecutar el create table
de la tabla doctor
de a mero arriba.
Tablas transaccionales y tablas de catálogos
Antes de crear en orden las tablas para que no nos salgan estos errores, debemos de explicar la diferencia entre algunos tipos de tablas para tener claro nuestro diseño de BD.
Ya hemos cubierto las tablas históricas cuando explicamos las llaves compuestas. Ahora vamos a explicar la diferencia entre las tablas transaccionales y las tablas de catálogo.
Tablas transaccionales
Son tablas donde guardamos eventos y transacciones del problem domain: un ingreso hospitalario, una verificación de coche, una venta, una compra, un check-in en un hotel, un login a un sistema, un ticket de soporte, etc.
Como tal, estas tablas transaccionales tienen 2 atributos importantes, su llave primaria, y una fecha transacción.
Adicionalmente, la frecuencia con la que escribimos en esas tablas nos da un indicio de la velocidad de nuestro negocio. Pensemos en una tabla hipotética VENTA
en diferentes escenarios:
- En una tiendita de la esquina:
- En un Oxxo (en su única caja abierta):
- En Liverpool:
- En un concesionario Tesla:
- En Amazon.com:
- En Netflix:
Esto también nos debe introducir concerns de volumetría (espacio en disco), en desempeño de lectura (índices, que veremos más tarde) y, más basicote (como Kim Kardashian), si la longitud de nuestra llave primaria y su respectiva secuencia dan lo suficiente como para sostener el ritmo de inserción en esta tabla. Si nuestra llave primaria está definida como integer(5)
, es decir, entera de 5 posiciones, esperaríamos que el máximo número que insertaremos es el 99999
. Esto no es así, debido a que el máximo número representable por el tipo integer
es 32767
, y entonces vamos a tener errores de inserción y por tanto pérdida de datos mucho antes que lleguemos a la capacidad máxima de la longitud de nuestra llave primaria.
En la BD de Sakila, podemos identificar 2 tablas de este tipo:
Las tablas de rental
y payment
registran transacciones, y es sensato suponer que tenemos 1 inserción cada 2 horas. Por tanto, son tablas que debemos monitorear de cerca para que tanto espacio, como longitud de campos, como tipo de datos, no nos vayan a sabotear.
Ahora, qué tal, la tabla customer
? Podemos considerarla transaccional? En qué escenarios si? En qué escenarios no?
Y la de film
?
Pero qué hay de los demás atributos? Como podemos ver, en el caso de rental
y payment
la mayoría son llaves foráneas, y aunque en el caso de film
y customer
hay otros atributos que le dan contexto, si las consideramos transaccionales, vale la pena ver qué llaves foráneas tenemos copiadas ahí, y por tanto, de qué relaciones 1 a N son parte.
Es muy probable que las otras tablas con las que estas tablas transaccionales tienen relación sean catálogos.
Tablas de catálogos
Estas tablas se caracterizan por tener una frecuencia de actualización lenta o nula, y casi siempre describen tipos de algo relevante al problem domain. Por ejemplo:
- elementos geográficos:
estado
,municipio
,codigo_postal
,colonia
,pais
. OJO: no se vayan con la finta de que no cambian. Si cambian. Cuando el DF se convirtió en CDMX muchos grupos y áreas de ingeniería de software sufrieron la gota gorda por no tener catálogos que pudieran cambiar fácilmente. - status de algún objeto del problem domain:
status_cliente
,status_envio
,status_transaccion
. - tipos o clases de algún objeto del problem domain:
tipo_estudio
,tipo_especialidad
,tipo_medicamento
.
Pueden existir relaciones entre catálogos, no es necesario que sean self-contained, si esto agrega contexto al problem domain. Por ejemplo:
Sin considerar las tablas de staff
y address
, que podemos considerarlas como transaccionales, city
y country
son 2 catálogos que tienen relación entre sí.
En la BD de Sakila podemos encontrar los siguientes catálogos:
Pero qué tal estos? Son catálogos?
Como buena práctica, si se actualizan o se agregan registros 2 veces al año o menos, serán catálogos.
Ejercicio
Entonces, con los conocimientos adquiridos, podemos decir que en el pequeño modelo E-R de ingreso hospitalario que imaginamos, el orden de creación puede ser:
tipo_especializacion
tipo_estudio
paciente
estudio
doctor
paciente_doctor
Creación de tablas intermedias para relaciones N a M
En particular para la tabla paciente_doctor
, que describe la relación N a M entre paciente
y doctor
, el comando SQL para su creación es un poco diferente porque las 2 llaves foráneas COMPONEN una sola llave primaria. El comando es:
--
-- TABLE: paciente_doctor
--
CREATE TABLE paciente_doctor (
id_paciente numeric(4) references paciente (id_paciente) ON UPDATE CASCADE ON DELETE CASCADE,
id_doctor numeric(4) references doctor (id_doctor) ON UPDATE cascade,
constraint pk_paciente_doctor primary key (id_paciente, id_doctor)
);
--
Las diferencias que encontramos con lo visto hasta ahorita son:
ON UPDATE CASCADE ON DELETE CASCADE
: esto se agrega al declarar una llave foránea para indicar qué hacer con el registro depaciente_doctor
cuando sucede unupdate
odelete
en la tablapaciente
. Es decir, si actualizamos algún atributo del paciente, se debe actualizar también la relación, y si borramos (DELETE) el registro enpaciente
, automáticamente la BD borrará el registro enpaciente_doctor
.constraint pk_paciente_doctor primary key (id_paciente, id_doctor)
: con esto estamos definiendo que la llave primaria se compone de ambos campos que fueron definidos como llaves foráneas en los comandos anteriores. Esta línea la pudimos haber ejecutado, en lugar de in_line, con unALTER TABLE ADD CONSTRAINT
.
Pueden encontrar el archivo completo para este ejercicio aquí: https://github.com/xuxoramos/db-4-ds/blob/gh-pages/doctor.sql