¿Cómo SELECCIONAR los cuatro elementos más nuevos por categoría?
-
22-07-2019 - |
Pregunta
Tengo una base de datos de artículos. Cada elemento se clasifica con una ID de categoría de una tabla de categorías. Estoy tratando de crear una página que enumere todas las categorías, y debajo de cada categoría quiero mostrar los 4 elementos más nuevos de esa categoría.
Por ejemplo:
Suministros para mascotas
img1
img2
img3
img4
Alimentos para mascotas
SELECT id FROM category
Sé que podría resolver fácilmente este problema consultando la base de datos para cada categoría de esta manera:
SELECT image FROM item where category_id = :category_id ORDER BY date_listed DESC LIMIT 4
Luego, iterar sobre esos datos y consultar la base de datos para cada categoría para obtener los elementos más nuevos:
<*>Lo que estoy tratando de averiguar es si solo puedo usar 1 consulta y obtener todos esos datos. Tengo 33 categorías, así que pensé que tal vez ayudaría a reducir la cantidad de llamadas a la base de datos.
Alguien sabe si esto es posible? O si 33 llamadas no es un gran problema y debería hacerlo de la manera más fácil.
Solución
Este es el mayor problema de n-por-grupo, y es una pregunta SQL muy común.
Así es como lo resuelvo con uniones externas:
SELECT i1.*
FROM item i1
LEFT OUTER JOIN item i2
ON (i1.category_id = i2.category_id AND i1.item_id < i2.item_id)
GROUP BY i1.item_id
HAVING COUNT(*) < 4
ORDER BY category_id, date_listed;
Supongo que la clave principal de la tabla item
es item_id
, y que es una pseudoclave que aumenta monotónicamente. Es decir, un valor mayor en item_id
corresponde a una fila más nueva en item
.
Así es como funciona: para cada elemento, hay algunos otros elementos que son más nuevos. Por ejemplo, hay tres elementos más nuevos que el cuarto elemento más nuevo. Hay cero elementos más nuevos que el elemento más nuevo. Por lo tanto, queremos comparar cada elemento ( i1
) con el conjunto de elementos ( i2
) que son más nuevos y tienen la misma categoría que i1
. Si el número de esos elementos más nuevos es inferior a cuatro, i1
es uno de los que incluimos. De lo contrario, no lo incluya.
La belleza de esta solución es que funciona sin importar cuántas categorías tenga, y continúa funcionando si cambia las categorías. También funciona incluso si el número de elementos en algunas categorías es inferior a cuatro.
Otra solución que funciona pero se basa en la función de variables de usuario de MySQL:
SELECT *
FROM (
SELECT i.*, @r := IF(@g = category_id, @r+1, 1) AS rownum, @g := category_id
FROM (@g:=null, @r:=0) AS _init
CROSS JOIN item i
ORDER BY i.category_id, i.date_listed
) AS t
WHERE t.rownum <= 3;
MySQL 8.0.3 introdujo soporte para funciones de ventana estándar de SQL. Ahora podemos resolver este tipo de problema de la misma manera que otros RDBMS:
WITH numbered_item AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY item_id) AS rownum
FROM item
)
SELECT * FROM numbered_item WHERE rownum <= 4;
Otros consejos
Esta solución es una adaptación de otra solución SO , gracias RageZ por ubicar este relacionado / pregunta similar.
NOTA
Esta solución parece satisfactoria para el caso de uso de Justin. Dependiendo de su caso de uso, puede consultar las soluciones de Bill Karwin o David Andres en esta publicación. ¡La solución de Bill tiene mi voto! Vea por qué, ya que pongo ambas consultas una al lado de la otra ;-)
El beneficio de mi solución es que devuelve un registro por categoría_id (la información de la tabla de elementos está "enrollada"). El principal inconveniente de mi solución es su falta de legibilidad y su creciente complejidad a medida que crece el número de filas deseadas (digamos que tiene 6 filas por categoría en lugar de 6). También puede ser un poco más lento a medida que aumenta el número de filas en la tabla de elementos. (Independientemente, todas las soluciones funcionarán mejor con un número menor de filas elegibles en la tabla de elementos y, por lo tanto, es aconsejable eliminar periódicamente o mover elementos más antiguos y / o introducir un indicador para ayudar a SQL a filtrar las filas antes)
Primer intento (¡no funcionó!) ...
El problema con este enfoque era que la subconsulta [con razón, pero mala para nosotros] produciría muchas filas, en base a los productos cartesianos definidos por las autouniones ...
SELECT id, CategoryName(?), tblFourImages.*
FROM category
JOIN (
SELECT i1.category_id, i1.image as Image1, i2.image AS Image2, i3.image AS Image3, i4.image AS Image4
FROM item AS i1
LEFT JOIN item AS i2 ON i1.category_id = i2.category_id AND i1.date_listed > i2.date_listed
LEFT JOIN item AS i3 ON i2.category_id = i3.category_id AND i2.date_listed > i3.date_listed
LEFT JOIN item AS i4 ON i3.category_id = i4.category_id AND i3.date_listed > i4.date_listed
) AS tblFourImages ON tblFourImages.category_id = category.id
--WHERE here_some_addtional l criteria if needed
ORDER BY id ASC;
Segundo intento. (¡funciona bien!)
Se agregó una cláusula WHERE para la subconsulta, lo que obliga a que la fecha indicada sea la más reciente, la segunda más reciente, la tercera más tardía, etc. para i1, i2, i3, etc., respectivamente (y también permite los casos nulos cuando hay menos de 4 artículos para una identificación de categoría dada). También se agregaron cláusulas de filtro no relacionadas para evitar mostrar entradas que se venden. o entradas que no tienen una imagen (requisitos adicionales)
Esta lógica supone que no hay valores listados de fechas duplicadas (para una categoría_id dada). De lo contrario, estos casos crearían filas duplicadas. Efectivamente, este uso de la fecha indicada es el de una clave primaria incrementada monotónicamente como se define / requiere en la solución de Bill.
SELECT id, CategoryName, tblFourImages.*
FROM category
JOIN (
SELECT i1.category_id, i1.image as Image1, i2.image AS Image2, i3.image AS Image3, i4.image AS Image4, i4.date_listed
FROM item AS i1
LEFT JOIN item AS i2 ON i1.category_id = i2.category_id AND i1.date_listed > i2.date_listed AND i2.sold = FALSE AND i2.image IS NOT NULL
AND i1.sold = FALSE AND i1.image IS NOT NULL
LEFT JOIN item AS i3 ON i2.category_id = i3.category_id AND i2.date_listed > i3.date_listed AND i3.sold = FALSE AND i3.image IS NOT NULL
LEFT JOIN item AS i4 ON i3.category_id = i4.category_id AND i3.date_listed > i4.date_listed AND i4.sold = FALSE AND i4.image IS NOT NULL
WHERE NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i1.date_listed)
AND (i2.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i2.date_listed AND date_listed <> i1.date_listed)))
AND (i3.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i3.date_listed AND date_listed <> i1.date_listed AND date_listed <> i2.date_listed)))
AND (i4.image IS NULL OR (NOT EXISTS (SELECT * FROM item WHERE category_id = i1.category_id AND date_listed > i4.date_listed AND date_listed <> i1.date_listed AND date_listed <> i2.date_listed AND date_listed <> i3.date_listed)))
) AS tblFourImages ON tblFourImages.category_id = category.id
--WHERE --
ORDER BY id ASC;
Ahora ... compare lo siguiente donde introduzco una clave item_id y uso la solución de Bill para proporcionar la lista de estos al '' exterior '' consulta. Puedes ver por qué el enfoque de Bill es mejor ...
SELECT id, CategoryName, image, date_listed, item_id
FROM item I
LEFT OUTER JOIN category C ON C.id = I.category_id
WHERE I.item_id IN
(
SELECT i1.item_id
FROM item i1
LEFT OUTER JOIN item i2
ON (i1.category_id = i2.category_id AND i1.item_id < i2.item_id
AND i1.sold = 'N' AND i2.sold = 'N'
AND i1.image <> '' AND i2.image <> ''
)
GROUP BY i1.item_id
HAVING COUNT(*) < 4
)
ORDER BY category_id, item_id DESC
En otras bases de datos, puede hacerlo utilizando la función ROW_NUMBER
.
SELECT
category_id, image, date_listed
FROM
(
SELECT
category_id, image, date_listed,
ROW_NUMBER() OVER (PARTITION BY category_id
ORDER BY date_listed DESC) AS rn
FROM item
) AS T1
WHERE rn <= 4
Desafortunadamente MySQL no es compatible con la función ROW_NUMBER
, pero puede emularla usando variables:
SELECT
category_id, image, date_listed
FROM
(
SELECT
category_id, image, date_listed,
@rn := IF(@prev = category_id, @rn + 1, 1) AS rn,
@prev := category_id
FROM item
JOIN (SELECT @prev := NULL, @rn = 0) AS vars
ORDER BY category_id, date_listed DESC
) AS T1
WHERE rn <= 4
Véalo trabajando en línea: sqlfiddle
Funciona de la siguiente manera:
- Intially @prev se establece en NULL y @rn se establece en 0.
- Para cada fila que vemos, verifique si category_id es el mismo que la fila anterior.
- En caso afirmativo, incremente el número de fila.
- De lo contrario, inicie una nueva categoría y restablezca el número de fila a 1.
- Cuando se completa la subconsulta, el paso final es filtrar para que solo se mantengan las filas con un número de fila menor o igual a 4.
no muy bonita pero:
SELECT image
FROM item
WHERE date_listed IN (SELECT date_listed
FROM item
ORDER BY date_listed DESC LIMIT 4)
Dependiendo de cuán constantes sean sus categorías, la siguiente es la ruta más simple
SELECT C.CategoryName, R.Image, R.date_listed
FROM
(
SELECT CategoryId, Image, date_listed
FROM
(
SELECT CategoryId, Image, date_listed
FROM item
WHERE Category = 'Pet Supplies'
ORDER BY date_listed DESC LIMIT 4
) T
UNION ALL
SELECT CategoryId, Image, date_listed
FROM
(
SELECT CategoryId, Image, date_listed
FROM item
WHERE Category = 'Pet Food'
ORDER BY date_listed DESC LIMIT 4
) T
) RecentItemImages R
INNER JOIN Categories C ON C.CategoryId = R.CategoryId
ORDER BY C.CategoryName, R.Image, R.date_listed
el siguiente código muestra una forma de hacerlo en un bucle definitivamente necesita mucha edición, pero espero que ayude.
declare @RowId int
declare @CategoryId int
declare @CategoryName varchar(MAX)
create table PART (RowId int, CategoryId int, CategoryName varchar)
create table NEWESTFOUR(RowId int, CategoryId int, CategoryName varchar, Image image)
select RowId = ROW_NUMBER(),CategoryId,CategoryName into PART from [Category Table]
set @PartId = 0
set @CategoryId = 0
while @Part_Id <= --count
begin
set @PartId = @PartId + 1
SELECT @CategoryId = category_id, @CategoryName = category_name from PART where PartId = @Part_Id
SELECT RowId = @PartId, image,CategoryId = @category_id, CategoryName = @category_name FROM item into NEWESTFOUR where category_id = :category_id
ORDER BY date_listed DESC LIMIT 4
end
select * from NEWESTFOUR
drop table NEWESTFOUR
drop table PART
Recientemente me encontré con una situación similar, probé una consulta que funcionó para mí y que es independiente de la base de datos
SELECT i.* FROM Item AS i JOIN Category c ON i.category_id=c.id WHERE
(SELECT count(*) FROM Item i1 WHERE
i1.category_id=i.category_id AND
i1.date_listed>=i.date_listed) <=3
ORDER BY category_id,date_listed DESC;
Es equivalente a ejecutar 2 para bucles y verificar si los elementos más nuevos son menos de 3
ok después de buscar en Google, la respuesta rápida no sería posible al menos en mysql
este este hilo para referencia
tal vez debería almacenar en caché el resultado de esa consulta si tiene miedo de que el servidor se caiga y desea que el código funcione mejor