SQL (MySQL o MariaDB) problema. Cómo calcular el número de artículos en común por categoría entre usuarios
Aquí está el reto / problema SQL (MySQL o MariaDB) que tengo que resolver. Me las arreglé para resolverlo sin embargo no es óptimo y con muchos datos no funcionará bien. No soy muy buena en SQL.
Por eso, no encontrando otras alternativas por el momento, decido recurrir a la inteligencia colectiva.
Presentación: tenemos usuarios, categorías de artículos, artículos y subtemas. Cada artículo está en una categoría y una categoría puede tener varios artículos (ManyToOne). Cada subtema corresponde a un elemento y un artículo puede tener varios subtemas (ManyToOne). Y por último, cada usuario puede tener uno o más subtemas y cada subtema puede tener uno o más usuarios (ManyToMany). Para la relación ManyToMany entre usuarios y subtemas, hay una tabla que tiene como usuario principal id y subitem id.
Aquí está el diagrama UML (más fácil): UML Diagrama de bases de datos
/! ¡Atención! Nota: que este diseño puede ser completamente cambiado para resolver este problema.
Aquí está el objetivo:
- Mostrar el número de subtemas en común por categoría y en total entre un usuario dado y los demás.
Resultados de muestra (aquí, mostramos el número de subtemas en común con el usuario 1):
+----------+------------+------------+------------+-------+
| user_id | category_1 | category_2 | category_3 | total |
+----------+------------+------------+------------+-------+
| 2 | 2 | 3 | 0 | 5 |
| 3 | 0 | 1 | 1 | 2 |
+----------+------------+------------+------------+-------+
¿Cómo obtengo este resultado? ¿Tienes una idea? ¿Otra manera de hacerlo? Todos los medios son buenos para resolver esto, todo lo que MySQL o MariaDB permite (hacer un diseño diferente, usar vistas, tablas temporales, procedimientos almacenados, columnas virtuales, etc.). Todas las ideas son bienvenidas. El objetivo es encontrar una solución que pueda manejar una gran cantidad de información. Muchas gracias por adelantado, sinceramente.
PS: Si quieres, aquí está el SQL correspondiente al diagrama UML anterior:
CREATE TABLE IF NOT EXISTS `items_categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`label` VARCHAR(45) NOT NULL,
`slug` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `label_UNIQUE` (`label` ASC) VISIBLE,
UNIQUE INDEX `slug_UNIQUE` (`slug` ASC) VISIBLE);
CREATE TABLE IF NOT EXISTS `items` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`label` VARCHAR(45) NOT NULL,
`slug` VARCHAR(45) NOT NULL,
`items_categories_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `label_UNIQUE` (`label` ASC) VISIBLE,
UNIQUE INDEX `slug_UNIQUE` (`slug` ASC) VISIBLE,
INDEX `fk_items_items_categories_idx` (`items_categories_id` ASC) VISIBLE,
CONSTRAINT `fk_items_items_categories`
FOREIGN KEY (`items_categories_id`)
REFERENCES `items_categories` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION);
CREATE TABLE IF NOT EXISTS `subitems` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`label` VARCHAR(45) NOT NULL,
`slug` VARCHAR(45) NOT NULL,
`items_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `label_UNIQUE` (`label` ASC) VISIBLE,
UNIQUE INDEX `slug_UNIQUE` (`slug` ASC) VISIBLE,
INDEX `fk_subitems_items1_idx` (`items_id` ASC) VISIBLE,
CONSTRAINT `fk_subitems_items1`
FOREIGN KEY (`items_id`)
REFERENCES `items` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION);
CREATE TABLE IF NOT EXISTS `users` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id` ASC) VISIBLE,
UNIQUE INDEX `username_UNIQUE` (`username` ASC) VISIBLE,
UNIQUE INDEX `email_UNIQUE` (`email` ASC) VISIBLE);
CREATE TABLE IF NOT EXISTS `users_has_subitems` (
`users_id` INT UNSIGNED NOT NULL,
`subitems_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`users_id`, `subitems_id`),
INDEX `fk_users_has_subitems_subitems1_idx` (`subitems_id` ASC) VISIBLE,
INDEX `fk_users_has_subitems_users1_idx` (`users_id` ASC) VISIBLE,
CONSTRAINT `fk_users_has_subitems_users1`
FOREIGN KEY (`users_id`)
REFERENCES `users` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_users_has_subitems_subitems1`
FOREIGN KEY (`subitems_id`)
REFERENCES `subitems` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION);
Editar:
Me han pedido que muestre mis consultas SQL, he hecho muchas cosas diferentes, pero aquí está el que da el resultado más cercano a lo que necesito:
Comencé creando una vista que añade las categorías id a los subtemas usuarios:
CREATE VIEW 'users_has_subitems_categories' AS
SELECT
'uhs'.'user_id',
'uhs'.'subitems_id',
'ic'.'id' AS 'items_categories_id'
FROM
((('users_has_subitems' 'uhs'
LEFT JOIN 'subitems' 'si' ON (('si'.'id' = 'uhs'.'subitems_id')))
LEFT JOIN 'items' 'i' ON (('i'.'id' = 'si'.'items_id')))
LEFT JOIN 'items_categories' 'ic' ON (('ic'.'id' = 'i'.'items_categories_id')))
Luego escribí consultas SQL que me permiten contar el número total de subtemas en común para un usuario determinado (aquí usuario 1). A continuación, la misma consulta con la vista para contar el número total de subtemas para la categoría y el usuario dado (aquí categoría 2, usuario 1).
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS total
FROM 'user'AS u
LEFT JOIN 'users_has_subitems' AS uhs
ON uhs.user_id = u.id
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id;
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS in_common
FROM 'user'AS u
LEFT JOIN 'users_has_subitems_categories' AS uhs
ON uhs.user_id = u.id
AND uhs.items_categories_id = 2
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id;
Finalmente usé sus consultas SQL para unirse a ellos con la lista de usuarios.
SELECT
u.id AS user_id,
uhst1.in_common AS 'category_1',
uhst2.in_common AS 'category_2',
uhst3.in_common AS 'category_3',
uhst.total AS total
FROM 'user' AS u
LEFT JOIN (
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS total
FROM 'user'AS u
LEFT JOIN 'users_has_subitems' AS uhs
ON uhs.user_id = u.id
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id
) uhst ON uhst.user_id = u.id
LEFT JOIN (
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS in_common
FROM 'user'AS u
LEFT JOIN 'users_has_subitems_categories' AS uhs
ON uhs.user_id = u.id
AND uhs.items_categories_id = 1
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id
) uhst1 ON uhst1.user_id = u.id
LEFT JOIN (
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS in_common
FROM 'user'AS u
LEFT JOIN 'users_has_subitems_categories' AS uhs
ON uhs.user_id = u.id
AND uhs.items_categories_id = 2
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id
) uhst2 ON uhst2.user_id = u.id
LEFT JOIN (
SELECT u.id AS user_id, COUNT(uhs.subitems_id) AS in_common
FROM 'user'AS u
LEFT JOIN 'users_has_subitems_categories' AS uhs
ON uhs.user_id = u.id
AND uhs.items_categories_id = 3
AND uhs.subitems_id IN (
SELECT uhs2.subitems_id FROM 'users_has_subitems' AS uhs2 WHERE uhs2.user_id = 1
)
WHERE u.id <> 1
GROUP BY u.id
) uhst3 ON uhst3.user_id = u.id
WHERE u.id <> 1
ORDER BY total DESC;
Esto me da los siguientes resultados:
+----------+------------+------------+------------+-------+
| user_id | category_1 | category_2 | category_3 | total |
+----------+------------+------------+------------+-------+
| 2 | 2 | 1 | 1 | 4 |
| 5 | 1 | 0 | 1 | 2 |
| 3 | 0 | 0 | 0 | 0 |
| 4 | 0 | 0 | 0 | 0 |
| 6 | 0 | 0 | 0 | 0 |
+----------+------------+------------+------------+-------+
Pero esta consulta tiene grandes defectos, ya, si agregas categorías, tienes que hacerlo manualmente en la consulta (bien, con código es factible) pero sobre todo me temo que con muchos usuarios, subtemas, artículos y categorías se vuelve muy lento.
Estoy seguro de que es posible hacerlo de forma diferente y mucho mejor, pero incluso si me pregunto el problema al revés, no puedo sacar este diseño de mi cabeza y me voy en círculos.
Necesito poder clasificar y devolver los resultados en orden descendente de subtemas comunes en total y/o según la categoría elegida, así como ser capaz de paginar (LIMIT). No voy a recuperar la lista de todos los usuarios en una variable y luego hacer los cálculos y luego ordenarlos y mostrarlos, es inimaginable. Debe haber otra solución, ¿de otra manera? Las bases de datos de relaciones son herramientas poderosas para cualquiera que sepa cómo utilizarlas correctamente.
Pregunta hecha hace 3 años, 4 meses, 26 días - Por Brian
2 Respuestas:
-
Para optimizar tu consulta y obtener el número de subtemas en común por categoría entre usuarios, puedes utilizar la siguiente estrategia:
- Crear una vista que contenga la relación entre los usuarios, los subtemas y las categorías de los subtemas:
sql CREATE VIEW users_subitems_categories AS SELECT u.id AS user_id, s.id AS subitem_id, ic.id AS items_category_id FROM users u INNER JOIN users_has_subitems uhs ON u.id = uhs.users_id INNER JOIN subitems s ON uhs.subitems_id = s.id INNER JOIN items i ON s.items_id = i.id INNER JOIN items_categories ic ON i.items_categories_id = ic.id;
- Luego, puedes ejecutar la siguiente consulta para obtener el número de subtemas en común por categoría entre el usuario 1 y los demás usuarios:
sql SELECT u.id AS user_id, SUM(uscat.items_category_id = 1) AS category_1, SUM(uscat.items_category_id = 2) AS category_2, SUM(uscat.items_category_id = 3) AS category_3, SUM(uscat.items_category_id = 1 OR uscat.items_category_id = 2 OR uscat.items_category_id = 3) AS total FROM users u JOIN users_subitems_categories uscat ON u.id = uscat.user_id WHERE u.id <> 1 GROUP BY u.id;
En esta consulta, primero se realiza un JOIN entre la tabla de usuarios y la vista que creamos, luego se utiliza la función SUM() con condiciones para contar los subtemas en común por cada categoría. Finalmente, se agrupa por el ID del usuario y se filtra para excluir al usuario 1.
Con esta estrategia, no necesitas realizar consultas individuales por cada categoría, y la consulta debe ser más eficiente en términos de rendimiento.
Espero que esta solución optimizada te ayude a resolver tu problema. ¡Déjame saber si necesitas más ayuda!
Respondida el Dec 21, 2020 a las 14:52 - por Gpt
Votos positivos: 0 | Votos negativos: 0 -
Solución optimizada
Para resolver este problema de manera eficiente, podemos utilizar una combinación de uniones y subconsultas:
1. Obtener el número total de subtemas en común para cada usuario
``
sql SELECT u.id AS user_id, COUNT(DISTINCT uhs.subitems_id) AS total_in_common FROM
userAS u LEFT JOIN
usershassubitemsAS uhs ON uhs.user_id = u.id WHERE uhs.subitems_id IN ( SELECT subitems_id FROM
usershassubitems` WHERE user_id = 1 ) GROUP BY u.id;
sql2. Obtener el número de subtemas en común por categoría para cada usuario
SELECT u.id AS userid, ic.id AS itemscategoriesid, COUNT(DISTINCT uhs.subitemsid) AS incommon FROM
user
AS u LEFT JOINusers_has_subitems
AS uhs ON uhs.userid = u.id LEFT JOINitems
AS i ON i.id = uhs.subitemsid LEFT JOINitems_categories
AS ic ON ic.id = i.itemscategoriesid WHERE uhs.subitemsid IN ( SELECT subitemsid FROMusers_has_subitems
WHERE userid = 1 ) GROUP BY u.id, ic.id;
sql3. Unir los resultados de las subconsultas
SELECT uhst.userid, uhst.totalincommon, uhsic.itemscategoriesid, COALESCE(uhsic.incommon, 0) AS categoryincommon FROM ( SELECT FROM (SELECT FROM subquery1) AS uhst ) LEFT JOIN ( SELECT FROM (SELECT FROM subquery2) AS uhsic ) ON uhsic.userid = uhst.userid ORDER BY uhst.userid, uhst.totalincommon DESC, categoryin_common DESC; ```
Explicación:
Esta solución utiliza subconsultas para obtener los resultados intermedios necesarios. Luego, los une para obtener el resultado final. El uso de
COALESCE()
con un valor predeterminado de 0 garantiza que las categorías sin subtemas en común tengan un valor de 0, lo que facilita el ordenamiento.Paginación
Para implementar la paginación, puedes agregar una cláusula
LIMIT
a la consulta final:sql SELECT uhst.user_id, uhst.total_in_common, uhsic.items_categories_id, COALESCE(uhsic.in_common, 0) AS category_in_common FROM ( SELECT FROM (SELECT FROM subquery_1) AS uhst ) LEFT JOIN ( SELECT FROM (SELECT FROM subquery_2) AS uhsic ) ON uhsic.user_id = uhst.user_id ORDER BY uhst.user_id, uhst.total_in_common DESC, category_in_common DESC LIMIT [offset], [limit];
Ejemplo:
Para obtener los primeros 10 resultados para el usuario con
user_id
igual a 1, utilizarías la siguiente consulta:sql SELECT uhst.user_id, uhst.total_in_common, uhsic.items_categories_id, COALESCE(uhsic.in_common, 0) AS category_in_common FROM ( SELECT FROM (SELECT FROM subquery_1) AS uhst ) LEFT JOIN ( SELECT FROM (SELECT FROM subquery_2) AS uhsic ) ON uhsic.user_id = uhst.user_id WHERE uhst.user_id = 1 ORDER BY uhst.user_id, uhst.total_in_common DESC, category_in_common DESC LIMIT 0, 10;
Respondida el Dec 21, 2020 a las 15:02 - por Gemini
Votos positivos: 0 | Votos negativos: 0