Diseño
de bases de datos con SQL Server ¿ Claves naturales o artificiales
?
Pregunta
ésta difícil de dar una contestación sencilla, puesto
que muchas veces influye más el gusto del modelador que aspectos
técnicos. Sin embargo, en este pequeño artículo vamos
a intentar llegar a una conclusión, o al menos, exponer los pros
y los contras de escoger una u otra opción. Creo que puede ser
interesante porque no se van a tener en cuenta los puntos de vista de
profesionales muy renombrados, MVP's la mayoría de ellos, pero
sobre todo (y lo más importante), amigos míos ;-)
Veréis
que nos centraremos en los tipos de datos IDENTITY, por ser el ejemplo
más claro de clave artificial, aunque no el único.
Empecemos
con la polémica...
Bueno,
comencemos por el principio. No es descabellado pensar que las claves
naturales fueron las primeras que aparecieron, por ser las más
lógicas, más "naturales" (permitidme el juego
de palabras), por lo que leer la opinión de un "tal"
Joe Celko no estaría mal, ¿verdad?. Aunque está en
inglés, creo que se entiende lo suficientemente bien como para
no tener que traducirlo (y así no me ponéis en un compromiso
;-) ). Ahí va:
**********************************
The
IDENTITY column is one of these mistakes.
1) It is not part
of the SQL-92 Standard and it is highly proprietary to the Sybase family.
It is not portable -- not quite the same thing as proprietary, since you
can often translate one SQL dialect into another with a simple replacement
(i.e. the % operator becomes the MOD () function). So your code will not
move over to a new database.
2) IDENTITY looks
like a datatype, but it is not. Create a table with one column in it and
make it an IDENTITY column. Insert a number into the table and see what
happens. Try to set it to NULL. If you cannot insert, update and delete
all the columns, then this is not a table!
3) IDENTITY looks
like a constraint, but it is not. Try to create a table with two IDENTITY
columns and it fails. If you cannot add it to a column, then it is not
a constraint. It is possible to write a a set of constraints that prohibit
data from ever being put in the table (their predicate is always FALSE).
It is possible to write a a set of constraints that allow anything in
the table (their predicate is always TRUE). But no constraint can prohibit
the creation of the table itself -- that is a meta-constraint.
4) It is not relational.
Consider this statement on a table, Foo, which has an IDENTITY column.
Assume the query returns more than one row.
INSERT INTO Foo
(x)
SELECT a FROM Bar;
You will get a
result like this:
IDENTITY X
============
1 'a'
2 'b'
3 'c'
but if the query
changed an index or was put on the physical disk data page differently,
you might have gotten:
IDENTITY X
============
1 'b'
2 'c'
3 'a'
Explain why one
result is the logically correct choice for an identifier and all other
choices are not, without any reference to the physical implementation.
You cannot.
Instead of treating
the query as a set, you are doing 1950's sequential processing using the
underlying sequential file system the Sybase family started with.
5) If you have
designed your tables correctly, they will have a meaningful primary key
derived from the nature of the entity they model. The IDENTITY column
should be a redundant key. The reason IDENTITY columns are popular as
keys is that they are easy to declare. This is also the same reason that
people build non-normalized databases and put pennies in fuse boxes --
easy is not right.
6) It is a bitch
to do calculations on IDENTITY column values. Well, it was hard to do
direct math on the sequential position of a record in a 1950's punch card
system and that it what the IDENTITY is mimicking.
7) There is no
check digit in an IDENTITY columns value, so you have no way of verifying
it if you use it as a key.
8) If you use IDENTITY
as a key, the values tend to cluster on physical data pages because they
are sequential. The result is that if the most recent rows are the most
likely to be accessed, there will be locking contention for control of
those physical data pages. What you really wanted in a key is some spread
of the rows over physical storage to avoid having every user trying to
get to the same page at the same time.
9) The actual implementation
of the IDENTITY column has been problematic since Version 7.0. You can
look up threads in the news groups to get assorted tales of woe.
There are other
ways of getting a unique identifier for a table. The most portable method
for getting a new identifier number which is not in the set is something
like this:
INSERT INTO Foobar
(keycol, a, b, c...)
VALUES(COALESCE((SELECT MAX(keycol) FROM Foobar) +1, 0), aa, bb, cc, ...);
The scalar subquery
expression returns the current high value for the key column, and then
increments it. If there is no maximum value (i.e. this is the first row
to be inserted), then it returns zero. Using this basic idea, you can
replace the increment with a different constant or a random number generator.
You can also add code to create a check digit.
Another method
is to hash the columns that make up a compound key so that you have single
short column that can be reconstructed if you need to verify it.
**********************************
Uuuuuf
creo que le gustan, ¿verdad?. Bueno, veamos, ¿qué
es lo que podemos sacar en claro de esta larga explicación?.
- en primer lugar,
es obvio que un IDENTITY no es muy portable que digamos, al menos no
directamente. Sin duda es un problema grave que nos podemos encontrar
si a alguien se le ocurre la locura de migrar de SQL Server a
Oracle (pobrecito él)
- también
nos encontramos con el problema de que, aunque podemos definir una columna
como de este tipo de datos, luego en realidad no podemos manejarla como
tal. Resulta paradójico, y en ciertos casos puede resultar problemático
- la facilidad de
uso de estos datos y su prácticamente nulo mantenimiento ha hecho
que se hayan extendido hasta el punto de ser usados erróneamente,
dando lugar a malos diseños de tablas por no querer pensar más
las cosas
- no son imprescindibles,
ni mucho menos. Podemos conseguir su misma funcionalidad y aumentarla
con muy poco código, y sabremos en todo momento qué es
lo que está pasando
Bien, entonces, si parece que no dan más que problemas, ¿quién
los quiere usar después de esto?. Tranquilos, tranquilos, que no
todo va por este lado; vamos poquito a poco a ir replicando todas estas
pegas, a ver hasta dónde llegamos.
La
primera réplica está clara: no hay que ser tan dogmático.
Si nos atuviésemos siempre a lo que los libros dicen, seríamos
puros autómatas sin ninguna imaginación y, por tanto, no
lograríamos soluciones nuevas que pueden facilitarnos la vida en
ciertos casos. Lo estándar no puede llegar hasta los problemas
con los que nos encontramos diariamente por ser éstos muy concretos
(lógico, son nuestros y de nadie más), así que nada
impide que nos salgamos de lo que predica, en este caso, SQL-92,
y que nuestra solución sea mejor. ¿O es que a alguien se
le ha olvidado lo bien que viene la denormalización en ciertas
situaciones?.
Bueno,
parece que no todo son inconvenientes sobre los IDENTITY. ¿Volvemos
a utilizarlos o aún es demasiado pronto?. Por si alguien se ha
convencido simplemente con el párrafo anterior, veamos qué
ocurre con estos tipos de datos en las transacciones con un ejemplo:
/* Código
con IDENTITY */
DROP TABLE Test
GO
CREATE TABLE Test(
TestID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
TestName varchar(20) NOT NULL
)
GO
INSERT TEST VALUES ('Uno')
INSERT TEST VALUES ('Dos')
INSERT TEST VALUES ('Tres')
GO
-- Revisar los Valores
SELECT * FROM TEST
GO
-- Revisar el IDENTITY
DBCC CHECKIDENT(Test)
-- Hagamos una transacción
fallida
BEGIN TRAN
INSERT TEST VALUES ('Cuatr')
-- OOPS NOS EQUIVOCAMOS, VOLVAMOS ATRÁS CORRIENDO!!
ROLLBACK TRAN
GO
-- Menos mal que todo
volvió como estaba antes. Hagámoslo ahora correctamente.
INSERT TEST VALUES ('Cuatro')
SELECT * FROM TEST
-- ¿Pero qué ha pasado aquí? OOPS NO HAY ROLLBACK
EN LOS IDENTITY!!!
DBCC CHECKIDENT(Test)
/* Código sin
IDENTITY */
DROP TABLE TEST
GO
CREATE TABLE Test(
TestID INT --IDENTITY(1,1) NOT NULL PRIMARY KEY,
TestName varchar(20) NOT NULL)
GO
INSERT TEST SELECT COALESCE(MAX(TESTID),0)+1,
'Uno' FROM TEST
INSERT TEST SELECT COALESCE(MAX(TESTID),0)+1, 'Dos' FROM TEST
INSERT TEST SELECT COALESCE(MAX(TESTID),0)+1, 'Tres' FROM TEST
GO
SELECT * FROM TEST
GO
-- Vamos a ver qué
pasa ahora si nos equivocamos
BEGIN TRAN
INSERT TEST SELECT COALESCE(MAX(TESTID),0)+1, 'Cuatr' FROM TEST
-- Ya no sé si
fiarme
¿o sí?
ROLLBACK TRAN
GO
-- Qué nos mostrará...
¿
SELECT * FROM TEST
INSERT TEST SELECT COALESCE(MAX(TESTID),0)+1, 'Cuatro' FROM TEST
SELECT * FROM TEST
-- AHORA SÍ!!!!
¡¡Vaya!!
Resulta que ahora no me puedo fiar de los IDENTITY porque no quieren saber
nada de las transacciones. Problema gordo éste, no hay duda: los
valores se pierden y no se pueden recuperar. Está claro entonces:
no hay que usarlos, me puedo encontrar con problemas gordos y que además
no tienen solución. Los desecho totalmente... ¿seguro?.
Pongámonos
ahora del otro lado. ¿Os acordáis cuando decía que
las facilidades que nos brindaban este tipo de datos podían dar
lugar a usos erróneos?, pues el código anterior es un claro
ejemplo de ello. Es decir, dicho campo sólo está para simplificar,
y su valor no me debería importar mucho, salvo para hacer uniones
entre tablas. Veamos: si mi clave principal natural es empresa+serie+factura,
estos datos los mantengo, y hago que sean únicos, y además
añado un campo IDENTITY. El usuario nunca verá este campo
IDENTITY, pero yo lo utilizaré internamente para hacer JOIN con
él y así unir esta tabla con otras. En ese caso poco me
importa ese número o que me queden huecos al no hacer ROLLBACK
de él. De este modo obtenemos dos puntos a favor: la simplicidad
en las uniones y lo compacta que es la clave primaria de la tabla (no
es lo mismo 20 caracteres que los 4 bytes que ocupa un entero en SQL Server)
Una
página de índice con una clave de 10 bytes podrá
almacenar, simplificando, unas 300 entradas por página (8000/(10+14)),
mientras que un índice con una clave de 4 bytes podría almacenar,
simplificando de nuevo, unas 400 entradas por página (8000/(4+14)).
Esto representa una diferencia considerable, que permitiría en
algunas operaciones una diferencia de rendimiento apreciable.
Otro
caso distinto es la ejecución de JOIN, en cuyo caso el procesador
debe realizar una serie de comparaciones de valores extraídos de
los campos de enlace en ambas tablas. Con procesadores de 32 bits, cualquier
valor mayor de 4 bytes representa bien una comparación externa
al procesador, bien una operación que requiere más de un
ciclo de CPU, lo cual representa en cualquier caso una merma de rendimiento.
Sin embargo, en este caso, como en el de claves foráneas, los valores
almacenados en esos campos son completamente irrelevantes para el usuario,
y van a ser utilizados solamente por SQL Server para validar entradas
y para ejecutar enlaces, por lo que un campo IDENTITY podría ser
perfectamente válido.
Como
parece que estamos cogiendo carrerilla, vamos a seguir defendiendo a los
IDENTIY, o más en general a las claves artificiales, porque vamos
a recordar que en SQL Server también tenemos los tipos de datos
UNIQUEIDENTIFIER (GUID's). Y es que un valor de cualquiera de estos tipos
de datos no cambia nunca, justamente porque nos es irrelevante (no como
un código de oficina o de artículo de nuestra empresa, que
puede que a alguien se le ocurra la "magnífica" idea
de modificarlo). Esto es importante en la concurrencia del bloqueo optimista.
Veámoslo con un ejemplo. Supongamos que tenemos una la tabla con
una clave que pueda cambiar:
Clave
Campo1 Campo2
C1 V11 V12
C2 V21 V22
El
usuario A y el usuario B leen ambos el registro C1:
SELECT
* FROM Tabla
El
usuario A cambia el valor de la clave:
UPDATE
Tabla SET Clave = C0 WHERE Clave =C1
Entonces
el usuario B intenta cambiar el valor del campo1:
UPDATE
Tabla SET Campo1 = V00 WHERE Clave = C1.
¿Qué
ocurre?, simplemente no puede actualizar el registro porque no existe;
peor aún, no puede refrescar el registro porque el usuario B no
tiene nada que lo identifique. Con las claves artificiales esto no pasa,
ya que las claves artificiales no cambian. No es un inconveniente que
no se puedan cambiar, es una ventaja. Si ejecutas una instrucción
como ésta:
UPDATE
Tabla SET Campo1 = Valor1 WHERE Id = ValorId
Siendo
Id la clave artificial, puedes estar seguro de que si la instrucción
no actualiza ningún registro es porque el registro se ha eliminado,
no quedando la posibilidad de que se haya modificado la clave primaria
ya que es artificial.
Está
claro que el punto anterior es una ventaja, y eso sin contar que el pequeño
tamaño de un dato de tipo entero no es comparable con una PK formada
por varios campos alfanuméricos. Son dos razones prácticamente
no discutibles desde este punto de vista. ¿Entonces nos quedamos
con los IDENTITY?; bueno, puede ser un error desechar una clave natural
simplemente por su tamaño (10, 100 ó 200 bytes). Sí,
sí, leer atentamente lo siguiente y os sorprenderéis:
Supuestos:
| Tablas |
Llave natural |
Llave artificial |
| Empresas |
4 |
NR |
| Almacenes |
4 |
NR |
| Artículos |
20 |
4 (int) |
| Envase |
20 |
4 (int) |
| Carton |
20 |
4 (int) |
| LoteProd (bigint) |
50 |
8 |
En
alguna tabla (la llamaremos 'ArticulosProducidos') se requiere que estas
tablas estén relacionadas y juntas producen una llave primaria.
Asumamos que esa tabla tiene 5 Millones de filas. Por ultimo todas las
tablas requieren 90 bytes en otras columnas.
Esta
tabla 'ArticulosProducidos', se ve así en índices agrupados:
| Tipo |
#Pag |
Kb |
Niv. |
| Natural |
1,965 |
15,467 |
3 |
| Artificial |
258 |
2,031 |
2 |
Es
decir, nos ahorramos un 80% en el índice agrupado y un 33% en el
número de páginas requeridas para leer una fila, y como
son sólo referencias a otras tablas no tienes que crear un índice
UNIQUE (que sería mortal porque consumiría ¡¡728
Mb!!). Pero eso no es lo mejor: si revisamos las páginas de datos
se reducen sustancialmente, ya que ahora pasan de consumir 1 Gb a sólo
578 Kb: un ahorro del ¡¡¡30%!!!.
Si
en tu empresa o a tu cliente, se les ocurre contratar a Joe Celko para
validar tu diseño de BD, y sale con el cuento de ANSI, teoría
de BD, etc. puedes rápidamente sacar tus cálculos y decirle
a tu jefe o cliente:
- "Sí,
efectivamente decidí romper XXXXX para lograr un incremento del
100% en el desempeño, ahora si usted quiere bajar la velocidad
a la mitad, puedo volverla a normalizar."
Todos
sabemos la respuesta...
Pero
no cantes victoria, ahora contratan a Emilio Boucau, para que analice
desempeño, y Emilio sale con que la consulta mas frecuente es la
'ArticulosProducidos' filtrados por Artículo, Envase y Cartón
y cuando se hace dicha consulta (por llaves naturales que es lo que conoce
el usuario) escribiste:
SELECT
...
FROM ArticulosProduccion AS AP JOIN Articulos AS A ON AP.ArticuloID=A.ArticuloID
JOIN Envases AS E ON AP.EnvaseID=E.EnvaseID JOIN Cartones as C ON AP.CartonID=C.CartonID
WHERE A.Articulo=@Articulo AND E.Envase=@Envase AND C.Carton=@Carton
Con
lo cual se produce una búsqueda en cada uno de los índices
de dichas tablas para luego buscar en la tabla de 'ArticulosProducidos',
produciendo una lectura de 8 páginas (3 Tablas x 2 Niveles + 2
del índice sobre la llave Artificial), mientras que si hubieras
mantenido el esquema relacional puro sólo hubieras tenido que leer
3 páginas haciendo que el plan de ejecución de tu consulta
se vea horroroso.
Pero
aún así tienes las de ganar porque vuelves a sacar tus números
y dices:
- "Es correcto,
pero los índices de dichas tablas ocupan 3.6 Mb y por la frecuencia
que se usan normalmente están en memoria RAM, mientras que si
uso una llave Primaria Natural ocupa 15 Mb, esto sin contar con la diferencia
de espacio en los datos que produce un ahorro de 30%, si usted quiere
bajar la velocidad en un ...!."
¡¡¡Felicidades!!!
Tienes un diseño a prueba Joe y Emilio. Pero creo que igual te
van a echar porque si contratan a Emilio y a Joe para que valide tus diseños
es porque están buscando una excusa :)
Conclusiones
Ah,
pero... ¿hay conclusiones después de todo esto?. Bueno,
creo que hay una cosa clara: es un tema polémico, ¿verdad?
;-)
No,
hablando en serio, lo que podemos decir es que, como en muchos otros temas
de las bases de datos, no existe una receta mágica ni algo que
se pueda aplicar en todos los casos y que sea la mejor solución.
Y es que estamos hablando de diseño, y ya sabemos todos lo que
eso significa: muchas horas de análisis y de leer y leer los requisitos
del usuario.
Concretando
en nuestro tema, no se puede rechazar ninguna de las dos opciones tajantemente
porque, tal y como hemos visto en este (largo) artículo, tienen
ventajas e inconvenientes. Como hemos dicho antes, habrá que analizar
la situación en la que nos encontremos cuando nos enfrentemos a
este problema decidir por una u otra.
La
idea de este artículo ha sido intentar tener más claro lo
que supone usar las claves naturales o las claves artificiales a partir
de los comentarios de grandes profesionales de SQL Server. Espero
haberlo conseguido...
|