El día de hoy veremos cómo extraer datos de 2 tablas SQL distintas, combinarlos y extraer solo los primeros de ellos a partir de un parámetro de ordenamiento.
El ejemplo que mostraré va a mostrar los productos más vendidos en una tienda, pero esos productos se venden al contado o en apartado, y cada uno está en una tabla distinta.
Lo que se requiere es obtener los productos más vendidos sin importar a cuál tabla pertenecen, ordenándolos, combinándolos y extrayendo solo los primeros N.
Explicación del algoritmo
No sé si existe una consulta SQL para combinar y ordenar datos de distintas tablas. No se me ocurre, así que mejor he usado el lenguaje de programación. El algoritmo queda así:
- Obtener los datos de la tabla 1, ordenados de mayor a menor
- Obtener los datos de la tabla 2, ordenados de mayor a menor
- Combinar los datos de la tabla 1 con la 2. Recorremos los datos de la tabla 2, si el dato ya existe en los datos de la tabla 1, aumentamos su cantidad (cantidad vendida en este caso) y si no lo agregamos a los datos de la tabla 1.
- Ordenamos los datos por la cantidad vendida
- Limitamos los datos a la cantidad deseada. Por ejemplo, solo obtenemos los primeros 10 y descartamos los demás.
Veamos cómo hacerlo con un ejemplo práctico con Golang.
Nota: el “motor” de base de datos que uso es SQLite3.
Ejemplo con Go
Primero veamos cómo obtener los datos por separados según cada tabla:
func (d *DatosGraficasController) productosMasVendidosAlContado(fechaInicio, fechaFin string) []ProductoVendidoParaGrafica {
db, err := d.AjustesUsuario.obtenerBaseDeDatos()
if err != nil {
panic(err)
}
defer db.Close()
filas, err := db.Query(`SELECT SUM(productos_vendidos.cantidadVendida) AS vecesVendido,
productos_vendidos.idProducto,
productos_vendidos.codigoBarras,
productos_vendidos.descripcion
FROM productos_vendidos
INNER JOIN ventas_contado
ON productos_vendidos.idVenta = ventas_contado.idVenta
AND ventas_contado.fecha
BETWEEN ?
AND ?
GROUP BY productos_vendidos.idProducto,codigoBarras, descripcion
ORDER BY vecesVendido DESC LIMIT ?;`, fechaInicio, fechaFin, LimiteProductosMasVendidos)
if err != nil {
log.Printf("Error al consultar productos más vendidos:\n%q", err)
}
defer filas.Close()
productos := []ProductoVendidoParaGrafica{}
for filas.Next() {
var producto ProductoVendidoParaGrafica
err = filas.Scan(&producto.VecesVendido, &producto.IdProducto, &producto.CodigoBarras, &producto.Descripcion)
if err != nil {
log.Printf("Error al escanear producto más vendido:\n%q", err)
}
productos = append(productos, producto)
}
return productos
}
func (d *DatosGraficasController) productosMasVendidosEnApartados(fechaInicio, fechaFin string) []ProductoVendidoParaGrafica {
db, err := d.AjustesUsuario.obtenerBaseDeDatos()
if err != nil {
panic(err)
}
defer db.Close()
filas, err := db.Query(`SELECT SUM(productos_apartados.cantidadVendida) AS vecesVendido,
productos_apartados.idProducto,
productos_apartados.codigoBarras,
productos_apartados.descripcion
FROM productos_apartados
INNER JOIN apartados
ON productos_apartados.idApartado = apartados.idApartado
AND apartados.fecha
BETWEEN ?
AND ?
AND apartados.abonado + apartados.anticipo >= apartados.monto
GROUP BY productos_apartados.idProducto,productos_apartados.codigoBarras, productos_apartados.descripcion
ORDER BY vecesVendido DESC LIMIT ?;`, fechaInicio, fechaFin, LimiteProductosMasVendidos)
if err != nil {
log.Printf("Error al consultar productos más vendidos:\n%q", err)
}
defer filas.Close()
productos := []ProductoVendidoParaGrafica{}
for filas.Next() {
var producto ProductoVendidoParaGrafica
err = filas.Scan(&producto.VecesVendido, &producto.IdProducto, &producto.CodigoBarras, &producto.Descripcion)
if err != nil {
log.Printf("Error al escanear producto más vendido:\n%q", err)
}
productos = append(productos, producto)
}
return productos
}
Fíjate que una tabla es productos_vendidos
y otra tabla es productos_apartados
. La consulta es prácticamente la misma, solo que de distintas tablas. Ambas son ordenadas por la cantidad vendida.
Los obtenemos a continuación y los combinamos, luego los ordenamos con quicksort y finalmente los cortamos en caso de que superen la cantidad deseada:
func (d *DatosGraficasController) productosMasVendidos(fechaInicio, fechaFin string) []ProductoVendidoParaGrafica {
// Nota: "masVendidosContado" se usa para mezclar tanto los vendidos al contado como apartados, se usa para evitar declarar un temporal
masVendidosApartados := d.productosMasVendidosEnApartados(fechaInicio, fechaFin)
masVendidosContado := d.productosMasVendidosAlContado(fechaInicio, fechaFin)
for _, producto := range masVendidosApartados {
indice := existeProducto(masVendidosContado, producto)
if indice != -1 {
masVendidosContado[indice].VecesVendido += producto.VecesVendido
} else {
masVendidosContado = append(masVendidosContado, producto)
}
}
quicksort(&masVendidosContado, 0, int64(len(masVendidosContado)-1))
if len(masVendidosContado) > LimiteProductosMasVendidos {
return masVendidosContado[:LimiteProductosMasVendidos]
} else {
return masVendidosContado
}
}
Por cierto, la función existeProducto
debería llamarse indiceProducto
y devuelve el índice de un producto en un arreglo o -1
si no existe:
func existeProducto(productos []ProductoVendidoParaGrafica, productoBuscado ProductoVendidoParaGrafica) int {
for indice, producto := range productos {
if producto.IdProducto == productoBuscado.IdProducto {
return indice
}
}
return -1
}
Y finalmente aquí tenemos la función quicksort que ordena ese arreglo de tipo Struct:
func particion(arreglo *[]ProductoVendidoParaGrafica, izquierda, derecha int64) int64 {
var pivote = (*arreglo)[izquierda]
for {
for (*arreglo)[izquierda].VecesVendido > pivote.VecesVendido {
izquierda++
}
for (*arreglo)[derecha].VecesVendido < pivote.VecesVendido {
derecha--
}
if izquierda >= derecha {
return derecha
} else {
(*arreglo)[izquierda], (*arreglo)[derecha] = (*arreglo)[derecha], (*arreglo)[izquierda]
izquierda++
derecha--
}
}
}
func quicksort(arreglo *[]ProductoVendidoParaGrafica, izquierda, derecha int64) {
if izquierda < derecha {
indiceParticion := particion(arreglo, izquierda, derecha)
quicksort(arreglo, izquierda, indiceParticion)
quicksort(arreglo, indiceParticion+1, derecha)
}
}
Por cierto, estoy usando apuntadores en Go aunque más tarde descubrí que no son necesarios y que podemos modificar un arreglo a partir de su índice cuando se pasa a una función… pero ya no quise probar y modificar, además, si funciona no lo toques.
Si alguien conoce alguna manera de hacerlo con puro SQL agradecería si coloca su solución en los comentarios.