Como comenté en el artículo anterior, tuve la oportunidad de tener cierto contacto con el sistema utilizado por la empresa Magic Software Argentina (MSA) en el escrutinio provisorio de las elecciones municipales de mi ciudad, Río Cuarto.
Los fiscales informáticos tuvimos acceso a la interfaz de consulta del proceso de escrutinio y a un volcado de la base de datos, que se realizaba cada 5 minutos. Lamentablemente, no tuvimos acceso al código fuente de la aplicación (aunque espero poder acceder al informe de los peritos contratados por la Junta Electoral).
Una vez finalizado el escrutinio provisorio, surgieron diferencias en los totales de votantes de los dos cargos electivos. A raíz de esto, decidí inspeccionar la base de datos con el objetivo de generar el listado de las mesas en las cuales se originaba dicha diferencia. De dicho análisis resultó el siguiente artículo, en el que me permito realizar algunas críticas.
El diagrama de Entidad-Relación (DER)
El siguiente es el DER que nos envió la gente de MSA:

Como puede verse se trata de una base de datos realmente simple (apenas 11 tablas). Lo que me llamó la atención a primera vista fue la utilización de palabras en castellano (muchos programadores opinamos que los programas deberían estar documentados en inglés). Siguiendo con los nombres, por más que me esforcé no pude imaginar la diferencia entre “ubicaciones” y “lugares“. No encontré palabras como “mesa“, “escuela“, “circuito“. A mi entender, la elección de la nomenclatura no ha sido la mejor.
Las claves primarias
En 9 tablas de la base de datos, las claves primarias son del tipo “varchar“. Si bien esto no es del todo incorrecto, no es muy recomendable. Las claves primarias deberían ser del tipo “integer“, y además auto-incrementadas (PostgreSQL, el RDBMS utilizado, lo soporta). Esto no solamente simplifica la asignación de valores a las claves (no hay que verificar la unicidad), sino que tiene un gran impacto en el desempeño de la base de datos.
Al utilizar el tipo “varchar“, cuya longitud es variable, el tamaño de los índices es mayor que usando “integer“. Esto, al aumentar la cantidad de filas de las tablas, se torna cada vez más importante (es deseable que los índices quepan en la caché del RDBMS). (En Río Cuarto había solamente 352 mesas y menos de 110.000 votantes, por lo cual no debe haberse notado la diferencia, pero en una elección más amplia seguramente lo haría.)
Un factor que agrava la situación es que la comparación de claves, al tratarse de cadenas, debe hacerse caracter a caracter (a diferencia de la comparación de valores enteros, que se realiza mediante una simple resta). Como veremos más adelante, en algunas tablas los valores del atributo clave en varias filas tienen un prefijo en común. Esto ralentiza aún más la comparación, y la situación es empeorada por a la utlización de la codificación de caracteres UTF-8, debido a la implementación interna de PostgreSQL.
En mi opinión pudiendo usar valores enteros como clave, debieron haberlo hecho.
¿Relación inexistente?
Primero, una breve explicación del problema: La ciudad se divide en 3 circuitos electorales, en los cuales se encuentran las distintas escuelas, en donde a su vez están las mesas de votación. Como el sistema permite analizar circuitos, escuelas y mesas por separado, el primer diseño que cualquier programador pensaría es disponer de tres tablas: “circuitos”, “escuelas” y “mesas” (en el caso de una elección provincial o nacional se agregarían algunas más). Entre esas tablas habría relaciones 1:N, es decir “1 circuito tiene N escuelas, 1 escuela pertenece a un circuito”, “1 escuela tiene N mesas, 1 mesa pertenece a 1 escuela”.
Aprovechando el hecho de que tanto las escuelas como las mesas tienen los mismos atributos, decidieron usar una única tabla, denominada “ubicaciones”, según muestra el DER:

Es común utilizar este tipo de representación para estructuras de “árbol”. Esto se logra agregando como atributo una clave foránea que referencia a la clave primaria de la misma tabla. En las filas “padre” dicho atributo tiene el valor “NULL“, en tanto que en las filas “hijas”, dicho atributo contiene el valor de la clave primaria del padre.
Sin embargo, el código SQL de la estructura de la tabla “ubicaciones” es el siguiente:
CREATE TABLE ubicaciones ( id_ubicacion character varying(12) NOT NULL, clase character varying(30) NOT NULL, descripcion character varying(100), sexo character(1), CONSTRAINT ubicaciones_sexo_check CHECK (((sexo = 'F'::bpchar) OR (sexo = 'M'::bpchar))) );
No existe ningún atributo que oficie de clave foránea para representar la relación en cuestión. ¿Cómo es esto posible?
Para comprender cómo está representada esta relación, fue necesario analizar los datos de la tabla (algo que, después de discutirlo con varios amigos, no pudimos llegar a la conclusión de qué principios de la teoría de bases de datos violenta, quizás algún lector pueda echar algo de luz al respecto). Encontre lo siguiente:

El “truco” consiste en utilizar parte de la clave primaria de una fila, para referenciar a la fila “padre”. Por ejemplo, las claves de todas las mesas pertenecientes a la escuela “Adolfo Alsina” (cuya clave es “RC0101“) comienzan con el prefijo “RC0101“. De la misma manera, las claves de todas las escuelas del circuito “165″ (cuya clave es “RC01“) comienzan con el prefijo “RC01“.
Ingenioso, pero totalmente innecesario y contrario a la teoría de bases de datos relacionales. De hecho, la relación está presente en el DER, pero no se ve reflejada en la base de datos. (Este tipo de representación de relaciones basadas en códigos era una práctica común de los programadores Cobol de otras épocas.)
Tipos de datos
Siguiendo con la tabla “ubicaciones“, podemos apreciar la siguiente restricción de integridad:
CONSTRAINT ubicaciones_sexo_check CHECK (((sexo = 'F'::bpchar) OR (sexo = 'M'::bpchar)))
El objetivo de esta restricción es verificar que el valor del atributo “sexo” (de tipo “character(1)” sea “F” o “M“. Una forma más simple y eficiente, es definir un tipo de datos enumerado:
CREATE TYPE sexos AS ENUM ('F', 'M')
Esta misma situación se produce en otras tablas, incluyendo la reiteración de casos como el que sigue:
CONSTRAINT usuarios_carga_check CHECK (carga = 'SI'::bpchar OR carga = 'NO'::bpchar)
Aquí, evidentemente, hubiera bastado definir al atributo “carga” de tipo “boolean“. (Esta restricción se encuentra en la tabla “usuarios“, en donde es reiterada tres veces.)
La tabla “estados” contiene los distintos estados en que puede encontrarse una planilla a lo largo del proceso:
CREATE TABLE estados ( id_estado character varying(16) NOT NULL, descripcion character varying(50), bg_color1 character varying(7), bg_color2 character varying(7), prioridad integer, orden integer, accion character varying(20), CONSTRAINT estados_id_estado_check CHECK ((((((id_estado)::text = 'Espera'::text) OR ((id_estado)::text = 'En Proceso'::text)) OR ((id_estado)::text = 'Intranet'::text)) OR ((id_estado)::text = 'Internet'::text))) );
Aquí la restricción de integridad tiene como objetivo que restringir los valores de la clave “id_estado“, pero esto deja sólo cuatro posibilidades (de hecho, ¡los cuatro estados posibles de una planilla!). Codificar en el esquema de la base de datos los valores (fijos) de las claves de las cuatro entradas que luego tendrá la tabla no parece una buena idea. (Nuevamente, de juzgarse necesario, debería haberse utilizado un tipo enumerado.)
Otro punto cuestionable es la elección de los valores para codificar cada estado: varias palabras, uso de mayúsculas y minúsculas.
Otros errores
En el DER puede verse lo siguiente:

Esto parece indicar que no puede haber más de 2 planillas en el mismo estado, algo que es falso (de hecho, inicialmente todas las planillas están en el estado “Espera” y al finalizar están en el estado “Internet”.
El DER también muestra la siguiente entidad:

La tabla que se corresponde con dicha entidad se llama “configuracion” (en singular).
Estos errores pueden parecer simples detalles (de hecho, en un sistema con solamente 11 tablas posiblemente lo sean), pero en un sistema más complejo pueden dificultar sensiblemente la programación, propendiendo a la comisión de errores.
Conclusión
Sin ser un experto en bases de datos relacionales (las utilizo desde hace más de 10 años como parte de mi trabajo, pero no me considero un especialista en la materia), los errores detectados me dan la pauta de que no se ha puesto el suficiente cuidado en el diseño de la base de datos, fundamentalmente de cara a la escalabilidad del sistema para soportar volumenes de carga mayores a los generados por proceso electoral como el de Río Cuarto (2 cargos electivos, 6 listas y 352 mesas).
De más está decirlo, dejo abierto este espacio a críticas y sugerencias respecto de este tema.

Muy bueno Javier! Una pregunta, esta empresa MSA es conocida, tiene trayectoria? Porque por lo que contás, parecería que fue algo que se armó a las apuradas, seguramente en connivencia con algún miembro de la junta electoral que se llevó una tajada.
Es evidente que esta aplicación fue armada de un día para otro, y diseñada y programada por principiantes, lo que no es coherente ni con el precio cobrado ni con la importancia que tiene un sistema de esta naturaleza.