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.