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.

¿Fue útil?

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top