Función para calcular la distancia entre dos coordenadas en MySQL

Llevo un par de días trabajando con coordenadas geográficas en un proyecto personal, por lo que voy a hablar del tema estos días.

Para saber qué distancia separa dos coordenadas recurrimos a la fórmula del Haversine, o fórmula del semiverseno. Delego la parte matemática teórica y vamos a la parte MySQL, que supongo que es lo que estabas buscando cuando llegaste aquí. Os dejo el código y una pequeña explicación paso a paso. La función requiere cuatro variables (latitud y longitud de cada uno de los puntos) y devuelve el resultado en kilómetros.

/*vamos a inicializar nuestras variables,
en este caso con un valor constante
pero en vuestro caso lo podéis hacer
con datos sacados de una consulta Select*/

Set @Lat1 = 42.880501;
Set @Lng1 = -8.545732;
/*Son las coordenadas de la catedral de Santiago
o eso me ha dicho DuckDuckGo*/ 

Set @Lat2 = 53.731951;
Set @Lng2 = -6.877150;
/*Como segundo punto he elegido una calle cualquiera 
en Kells, Irlanda*/

/*Y ahora aplicamos la función SQL 
Que nos devolverá la distancia en kilómetros*/

SELECT (acos(sin(radians(@Lat1)) * sin(radians(@Lat2)) + 
cos(radians(@Lat1)) * cos(radians(@Lat2)) * 
cos(radians(@Lng1) - radians(@Lng2))) * 6371) as distanciaKm;

/*el resultado con estos datos es de 
1212.8306071122186 kilómetros*/

/*Si quieres el resultado el millas
cambia el valor 6371 en la función
por 3959*/

Próximamente veremos como hacerlo este cálculo con Javascript, que es mucho más divertido.

Script para gestión de backups de PostgreSQL en FreeBSD

No recuerdo si había comentado que estaba en plena migración tecnológica laboral, trabajando con el SGBD libre PostgreSQL y el sistema operativo (libre, of course) FreeBSD. Y el primero de los trasteos era simple ¿cómo automatizo los backups? Ya habíamos hablado de este tipo de cuitas alguna vez por aquí, así que vamos a ver cómo con un script, y haciendo uso de Cron, podemos lograrlo:

#!/bin/sh
# pgsql backups --datosde conexion
db_user="tu_usuario"
db_name="tu_base_de_datos"
db_dumpdir="/var/backups/carpeta"
date_format="`date +%Y-%m-%d.%H:%M`"

#ARRIBA CAMBIA LOS EJEMPLOS POR LOS DATOS QUE CORRESPONDAN. 
 
# BORRAMOS LOS BACKUPS MÁS VIEJOS
# PARA NO SATURAR
# EN ESTE CASO, LOS QUE TENGAN MÁS DE
# 4 SEMANAS

expire_time="+4w"
 
# Begin Script #
/usr/local/bin/pg_dump -U $db_user $db_name -f "$db_dumpdir/$db_name-$date_format.pgsql"
/usr/bin/find $db_dumpdir -d 1 -Btime $expire_time -delete

Con este ya tenemos un script que nos genera un backup de la base de datos en nuestra carpeta de copias de seguridad y además borra los que tengan más de cuatro semanas, para que no saturemos el disco duro. Y ahora, como dijimos antes, necesitamos Crontab para automatizar la copia. Asumamos que guardamos el script en la dirección /usr/local/scripts/pg_backup.sh, porque somos gente ordenadita (al menos a la hora de organizar discos duros). En nuestro caso ejecutaremos el script cada 6 horas:

# crontab -e
* /6 * * * /usr/local/scripts/pg_backup.sh > /dev/null 2>&1

Y con esto ya tenemos la cuestión de automatizar las copias de seguridad solventada.

Diferencias entre clave primaria y clave foránea

Vamos con una entrada de SQL básico, uno de los fundamentos del modelo relacional ¿Qué diferencia existe entre una clave primaria y una foránea?

En el diseño de una base de datos relacional la clave primaria es el campo, o conjunto de campos, que nos permite identificar de forma única un registro. Por así decirlo es como el DNI de esa tabla. Se trata de un valor, o grupo de valores, único que nos permitirá diferenciar un registro concreto. Podemos definir la clave primaria en el momento de la creación de la tabla, dentro de la sintaxis del CREATE TABLE, o a posteriori añadiendo la regla en un ALTER TABLE.

Por su parte la clave foránea es un campo, o conjunto de campos, que nos permite relacionar un registro de una tabla con otro, generalmente de una tabla distnta . Como ejemplo piensa en la clásica base de datos de una tienda, en la que tienes una tabla con productos, cada uno de los cuales tiene un código (que será la clave primaria), una descripción y un precio. Por otra parte tienes una tabla en la que registras las ventas, y podrías tener un código para cada una, la fecha, la hora y el producto. Para identificar el producto en esa tabla lo más práctico, para no repetir información, sería utilizar el código de la tabla de productos (la clave primaria de la primera tabla, que en esta se convierte en clave externa).

Una tabla puede tener relaciones con varias a través de distintas claves foráneas, e incluso referenciarse a si misma (clave foránea recursiva). Al igual que con la clave primaria podemos definir las claves foráneas dentro de la sintaxis de creación de la tabla o a posteriori con un ALTER TABLE.

Por tanto la diferencia es sencilla: la clave primaria identifica un registro único de una tabla. La clave foránea relaciona los datos de un registro de una tabla con los de otra, o con un registro distinto de la misma tabla.

Llamar a procedimientos almacenados en PHP

Una entrada que llevaba mucho tiempo en el TODO list, sobre trabajar con procedimientos almacenados con PHP.

Para empezar vamos a plantear un escenario en el que tenemos tres procedimientos almacenados en MySQL (dos selects y un insert) contra una tabla llamada productos, que tiene tres campos (id como clave primaria, descripcion con una descripción del producto, precio con un valor numérico decimal).

El SP de la primera select, que nos devuelve la descripción y el precio del producto según su id, sería este:

CREATE PROCEDURE getProducto(IN id_val INT) 
  BEGIN 
    SELECT descripcion, precio from productos WHERE id = id_val; 
  END;

El SP de la segunda select, que nos devuelve la descripción y el precio todos los productos, por lo que no recibe parámetros:

CREATE PROCEDURE getAllProductos() 
  BEGIN 
    SELECT descripcion, precio from productos ; 
  END;

Y el sp del insert sería este, al que le pasamos los tres valores a meter:

CREATE PROCEDURE addProducto(IN id_val INT,descripcion_in varchar(50), precio_in float) 
  BEGIN 
    INSERT INTO productos VALUES (id_val, descripcion_in, precio_in);
  END;

Bueno, ya tenemos nuestro escenario, entonces ¿cómo hacemos para llamar a estos procedimientos desde nuestro código php? Pues simplemente lanzamos una consulta en la que llamamos a la instrucción CALL de MySQL para que ejecute el procedimiento almacenado que le digamos con los parámetros que tocan.

/*Empezamos con el procedimiento de inserción*/
/*Lo primero es crear un objeto mysqli*/
$mysqli = new mysqli("servidor", "usuario", "pass", "db");

/*Y llamamos al procedimiento para hacer la inserción*/
/*Si falla imprimimos el error*/
if (!$mysqli->query("CALL addProducto(1001, 'Nueces de Macadamia', 12.23)")) {
    echo "Falló la llamada: (" . $mysqli->errno . ") " . $mysqli->error;
}

La propia función query del objeto mysqli nos devolverá TRUE si no viene con errores y FALSE si pasa algo inesperado.

¿Y qué pasa con la recuperación de datos? Ahí nos vamos a un terreno más pantanoso. Para una consulta con un solo resultado no es problema:

/*Empezamos con el procedimiento de recuperación de una fila*/
/*Lo primero es crear un objeto mysqli*/
$mysqli = new mysqli("servidor", "usuario", "pass", "db");

/*Y llamamos al procedimiento para recoger los datos*/
/*Si falla imprimimos el error*/
if (!($res = $mysqli->query("CALL getProducto(1001)"))) {
    echo "Falló la llamada: (" . $mysqli->errno . ") " . $mysqli->error;
}

/*E imprimimos el resultado para ver que el ejemplo ha funcionado*/
var_dump($res->fetch_assoc());

Pero claro, en este caso podemos hacerlo así porque esperamos un solo resultado, pero si esperamos varias filas la cosa es distinta ya que mysqli->query() devuelve sólo el primer conjunto de resultados. Tenemos dos alternativas para esta situación: mysqli_real_query() o mysqli_multi_query(). En el ejemplo usaremos la segunda, y veremos también la forma de recorrer este resultado:

/*Empezamos con el procedimiento de recuperación de una fila*/
/*Lo primero es crear un objeto mysqli*/
$mysqli = new mysqli("servidor", "usuario", "pass", "db");

/*Y llamamos al procedimiento para recoger los datos*/
/*Si falla imprimimos el error*/
if (!$mysqli->multi_query("CALL getAllProductos()")) {
    echo "Falló la llamada: (" . $mysqli->errno . ") " . $mysqli->error;
}

/*Ahora con este bucle recogemos los resultados y los recorremos*/
do {
    /*En el if recogemos una fila de la tabla*/
    if ($res = $mysqli->store_result()) { 
        /*Imprimimos el resultado de la fila y debajo un salto de línea*/
        var_dump($res->fetch_all());
        printf("\n");
        /*La llamada a free() no es obligatoria, pero si recomendable para aligerar memoria y para evitar problemas si después hacemos una llamada a otro procedimiento*/
        $res->free();
    } else {
        if ($mysqli->errno) {
            echo "Store failed: (" . $mysqli->errno . ") " . $mysqli->error;
        }
    }
} while ($mysqli->more_results() && $mysqli->next_result());
/*El bucle se ejecuta mientras haya más resultados y se pueda saltar al siguiente*/

Añadiré a esto que también es posible utilizar las llamadas a procedimientos almacenados usando sentencias preparadas, procediendo del modo habitual a la hora de pasar los parámetros.

Script para automatizar backups de MySQL en servidores Windows

Aunque en servidores Windows lo más habitual es trabajar con el SQL-Server de Microsoft como SGBD a veces puede tocarnos instalar un MySQL. Entonces nos asalta la duda ¿cómo automatizo un backup de la base de datos? Una de las tareas más habituales de todo administrador.

En la instalación de la base de datos se os instalará el programa mysqldump.exe para la gestión de copias de seguridad. Este programa nos permite, por medio de la línea de comandos, realizar un backup de la base de datos. Con la siguiente sintaxis, a grosso modo, haréis la copia de seguridad:

mysqldump –u (TuUsuario) –p(password) (nombre_base_datos) > (nombreArchivo).sql

Dato importante: entre -u y el nombre de vuestro usuario hay un espacio, entre -p y el password NO. Para más información sobre este comando, podéis visitar el artículo pertinente en el manual de MySQL.

Podéis abrir con CMD un terminal de comandos, situaroes en vuestra carpeta de MySQL y, situándoos allí en la carpeta bin, ejecutar el comando con los parámetros que correspondan para comprobar su funcionamiento. Una vez visto que mysqldump funciona el siguiente paso es crear un script de batch (con la extensión .bat) que ejecute el comando incluyendo la fecha. Los backups los guardaremos en una carpeta llamada C:\backups, y a la hora de escribir el script os vale cualquier editor, mismo el Notepad (aunque yo suelo usar en Windows Notepad++):

echo off                                        
set FECHA= %date%
set FECHA=%FECHA:/=%
mysqldump -e -u root –p7j91lt0y articulos > C:\backups\backuparticulos%FECHA%.sql

NOTA:Edito porque esta semana me he topado con que en el PowerShell de Windows 2k12 no me estaba funcionado esa instrucción, y también me han comentado hoy que el script no funcionaba. Lo mejor es que probéis primero en el intérprete de comandos si funciona, y si os falla la de arriba podéis cambiar la sintaxis por esta:

mysqldump --user=root --password=7j91lt0y articulos > C:\backups\backuparticulos%FECHA%.sql

Con la primera instrucción desactivamos la salida por pantalla de la consola, luego recogemos la fecha actual, la formateamos y finalmente creamos el backup anexando a su nombre la fecha de la copia. En este ejemplo he usado el usario root, recordad siempre que el usuario que utilicéis debe tener permisos en la base de datos para realizar la copia de seguridad. Guardad este script en la carpeta bin de vuestra instalación de MySQL.

Pues ya tenemos el script listo, ejecutadlo con permisos de administrador para ver que funciona. Si os crea un archivo vacío es que algo falla, repasadlo (la sintaxis, la contraseña, el nombre, los permisos tanto en la bd como en el sistema operativo…). Ahora lo que nos queda es automatizar la ejecución del script para programar, por ejemplo, una copia diaria de la base de datos. Para eso os váis al menú de administración del servidor y creáis una tarea básica que ejecute el batch a diario a una hora predeterminada (a poder ser una hora con poca actividad para la base de datos). Para más ayuda con el tema de la automatización podéis visitar la web de Microsoft.

Stored Procedure para guardar y devolver datos encriptados con AES (MySQL)

Esto llevaba un tiempo en el TODO list del blog, y a raíz de unos comentarios en el artículo sobre encriptación AES en MySQL y MariaDB he decidido hacerlo al fin. La cosa es que queremos guardar un dato encriptado en la base de datos pero de forma que sea transparente para el usuario, que cuando pide el dato lo recibe en texto plano.

El primer punto a la hora de guardar datos encriptados en AES es que debemos almacenarlo en un campo de tipo BLOB, ya que este algoritmo nos devolverá un resultado binario, no texto plano.

Por resumir vamos a imaginar una tabla que sólo tiene un Id autonumérico y un campo con un texto encriptado. Habrá dos procedimientos: uno para insertar los datos y otro para recuperarlos.

El procemiento que inserta sería así:

CREATE PROCEDURE sp_insertDatoEncriptado (IN `texto` varchar(128), IN `password` varchar(25))
DETERMINISTIC
BEGIN
    Insert Into 
      tabla (`TextoSecreto`)
    Values
      (AES_ENCRYPT(texto,password))
END

Y para recuperar los datos la cosa iría por este camino:

CREATE PROCEDURE sp_getDatoEncriptado (IN `password` varchar(25))
DETERMINISTIC
BEGIN
    Select
      Id,
      AES_DECRYPT(TextoSecreto, password) AS 'TextoSecreto'
    From
      tabla
END

Concatenar varios valores de la misma columna en una sola celda en SQL-Server

Esta tarde, para hacer un informe en una aplicación en el trabajo (SQL-Server+ASP+VB), necesitaba sacar todos los registros de una columna en un solo registro separado por un símbolo. Podéis decir que podría haber hecho una consulta normal y luego simplemente añadir las comas al imprimirla en pantalla, pero en este caso necesitaba que vinieran así en la consulta SQL.

Si bien en un principio pensé en utilizar un cursor, al final rebuscando por el manual de SQL-Server 2008 (que debe ser de los pocos que tengo en papel y no en PDF) encontré una solución que consume menos recursos.

Empecemos con la tabla, más o menos esta

CREATE TABLE Problemas(
Id INT PRIMARY KEY IDENTITY(1,1),
Incidencia VARCHAR(120),
Usuario INT,
Fecha DATETIME
)

Bien, la cosa es tener una cadena con todas las incidencias separadas por una «tubería» (el caracter | ).

El procedimiento va a ser el siguiente: declaramos una variable, hacemos un select donde vayamos concatenando cada resultado dentro de la variable, hacemos un apañico porque nos va a meter un separador al principio y acabamos con un select sobre la variable para mostrarla/recogerla.

--Declaramos la variable
DECLARE @ListaIncidencias VARCHAR(MAX) = ''
--Ahora vamos con la consulta
SELECT @ListaIncidencias = @ListaIncidencias +'|' + p.Incidencia
FROM Problemas p
--Eliminamos el separador extra que nos meterá al principio
--usando SUBSTRING
SET @ListaIncidencias = SUBSTRING(@ListaIncidencias,2,LEN(@ListaIncidencias))
--Hacemos un SELECT sobre la variable para sacarla por pantalla
SELECT @ListaIncidencias as 'Incidencias'

De esta forma todos los registros aparecerán como una cadena de texto dentro de una sola celda.

Diferencia entre inner join, left join y right join. SQL

Hoy otra cuestión a petición de un lector que decía que su nivel de inglés no era el suficiente para orientarse con la muy válida explicación que hay en StackOverflow que hasta incluye este útil diagrama:

Uniones entre tablas SQL
Diferentes tipos de uniones entre tablas

Si bien finalmente encontró una válida explicación en otras páginas (hay unas cuantas) me da pie para esta entrada. ¿Qué diferencia hay entre un inner join y un left o un right join?

Las más comunes suelen ser las uniones internas entre tablas (inner join), como ejemplo os diré que en los últimos seis meses, desde que empecé en mi actual trabajo, he tenido que hacer sólo 3 uniones externas mientras que internas he hecho docenas. Un inner join, si miráis el diagrama, se podría representar como la intersección entre dos conjuntos, es decir, una consulta con inner join mostraría los registros de las tablas que coincidan en los campos de la unión que se ha definido en dicha consulta. Dicho así es denso, pero con un ejemplo se entiende claro.

Supongamos que tenemos una tabla con los empleados de una empresa (con un identificador llamado Id, un campo de nombre, uno de apellidos y uno con el Id del departamento en que trabaja llamado IdDepartamento) y en otra tenemos los departamentos (Id y Nombre). Algunos trabajadores tienen un departamento asignado y otros no, y obviamente la relación entre ambas tablas se da entre el campo Id de Departamento con el campo IdDepartamento de la tabla Empleados. Utilizando un inner join podríamos obtener el listado de todos los trabajadores que tienen un departamento asignado:

select e.nombre, e.apellidos from empleados e inner join departamentos d on e.IdDepartamento=d.Id;

En cuanto al enlace externo tenemos dos opciones: left join y right join. Con esta unión recuperamos todos los enlaces de una tabla y los datos de la otra que coinciden con la condición definida en la consulta. Cuando usamos una unión left join recogemos todos los datos de la tabla que está a la izquierda de la unión en la consulta y si usamos en cambio right join las de la derecha. Imaginemos las tablas de antes, pero ahora queremos todos los datos de la tabla empleados, tengan o no departamento, y además el dato de en qué departamento están si lo tuvieran. Usaremos un left join para obtener los datos.

select e.nombre, e.apellidos from empleados e left join departamentos d on e.IdDepartamento=d.Id

En este caso obtendríamos los nombres y apellidos de todos los empleados, los departamentos de los que lo tenga y en los que no recibiríamos un campo con un valor NULL.

Finalmetne tendríamos opciones como el full outer join o producto cartesiano, que nos sacaría todos los valores de ambas tablas.

A partir de esto y mezclando varias tablas y condiciones extra en una cláusula WHERE podemos lograr resultados más complejos.

Consulta SQL para conocer el número de campos de una tabla

Por avatares del destino hoy estaba modificando una página en ASP clásico y necesitaba saber el número de campos que tenía una tabla. Basta con una consulta para tener estos datos:

SELECT Table_Name, COUNT(*) As NumeroCampos
FROM Information_Schema.Columns
WHERE Table_Name = 'nombre_de_la_tabla'
GROUP BY Table_Name;

Si queréis saber el de todas las tablas de la base de datos podéis hacer lo siguiente:

SELECT Table_Name, COUNT(*) As NumeroCampos
FROM Information_Schema.Columns
GROUP BY Table_Name
ORDER BY Table_Name

Hacer un UPSERT en MySQL

Si el otro día os contaba cómo hacer un INSERT IF NOT EXISTS en MySQL, hoy vamos a ver cómo hacer un UPSERT. La idea es intentar un INSERT, pero en caso de que se de una duplicidad de un campo único (una clave primaria, un campo UNIQUE…) en lugar de insertar actualizará el campo en cuestión. Hasta esta mañana desconocía esta forma de insertar, pero me lo comentaron en StackOverflow por un problemilla que tenía con un procedimiento almacenado.

La sintaxis sería más o menos:

INSERT INTO tabla (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1, b=2;

Os pongo un ejemplo práctico, metido dentro de un procedimiento almacenado. Recibe el id de un usurio y una variable bit que representa si se ha conectado o ha cerrado sesión, guarda estos datos junto con el momento en que se ha realizado dicha acción en una tabla usada para llevar el control de lo mismo (bueno, es más o menos lo mismo que me pusieron en SO):

DELIMITER $$
CREATE PROCEDURE `sp_UltimoLogin`(id_in int, accion_in bit)
BEGIN
  INSERT INTO `login` (`idusuario`, `fecha`, `accion`) VALUES (id_in, now(), accion_in)
    ON DUPLICATE KEY UPDATE `fecha` = now(), `accion` = accion_in;
END $$

Un truquito que os ahorrará realizar comprobaciones en muchos supuestos, aligerando la carga de trabajo y reduciendo líneas de código.