Tengo un patrón de consulta que debe ser muy común, pero no sé cómo escribir una consulta eficiente para él. Quiero buscar las filas de una tabla que corresponden a "la fecha más reciente, no después" de las filas de otra tabla.
Tengo una mesa, por inventory
ejemplo, que representa el inventario que tengo en un día determinado.
date | good | quantity
------------------------------
2013-08-09 | egg | 5
2013-08-09 | pear | 7
2013-08-02 | egg | 1
2013-08-02 | pear | 2
y una tabla, por ejemplo, "precio", que contiene el precio de un bien en un día determinado
date | good | price
--------------------------
2013-08-07 | egg | 120
2013-08-06 | pear | 200
2013-08-01 | egg | 110
2013-07-30 | pear | 220
¿Cómo puedo obtener de manera eficiente el precio "más reciente" para cada fila de la tabla de inventario, es decir
date | pricing date | good | quantity | price
----------------------------------------------------
2013-08-09 | 2013-08-07 | egg | 5 | 120
2013-08-09 | 2013-08-06 | pear | 7 | 200
2013-08-02 | 2013-08-01 | egg | 1 | 110
2013-08-02 | 2013-07-30 | pear | 2 | 220
Sé una forma de hacer esto:
select inventory.date, max(price.date) as pricing_date, good
from inventory, price
where inventory.date >= price.date
and inventory.good = price.good
group by inventory.date, good
y luego vuelva a unir esta consulta al inventario. Para tablas grandes, incluso hacer la primera consulta (sin volver a unirse al inventario) es muy lento. Sin embargo, el mismo problema se resuelve rápidamente si simplemente uso mi lenguaje de programación para emitir una max(price.date) ... where price.date <= date_of_interest ... order by price.date desc limit 1
consulta para cada uno date_of_interest
de la tabla de inventario, por lo que sé que no hay impedimento computacional. Sin embargo, preferiría resolver todo el problema con una sola consulta SQL, ya que me permitiría realizar más procesamiento SQL en el resultado de la consulta.
¿Hay una forma estándar de hacer esto de manera eficiente? Parece que debe aparecer a menudo y que debería haber una forma de escribir una consulta rápida para ello.
Estoy usando Postgres, pero agradecería una respuesta genérica de SQL.
\d tbl
en psql), su versión de Postgres y min. / max. cantidad de precios por bien.Respuestas:
Que depende mucho de las circunstancias y requisitos exactos. Considera mi comentario a la pregunta .
Solución simple
Con
DISTINCT ON
en Postgres:Resultado ordenado.
O con
NOT EXISTS
SQL estándar (funciona con todos los RDBMS que conozco):Mismo resultado, pero con un orden de clasificación arbitrario, a menos que agregue
ORDER BY
.Dependiendo de la distribución de datos, requisitos e índices exactos, cualquiera de estos puede ser más rápido.
En general,
DISTINCT ON
es el vencedor y obtienes un resultado ordenado además de él. Pero para ciertos casos, otras técnicas de consulta son (mucho) más rápidas todavía. Vea abajo.Las soluciones con subconsultas para calcular valores máximos / mínimos son generalmente más lentas. Las variantes con CTE son generalmente más lentas, todavía.
Las vistas simples (como propone otra respuesta) no ayudan en absoluto al rendimiento en Postgres.
SQL Fiddle.
Solución adecuada
Cuerdas y colación
En primer lugar, sufres de un diseño de tabla subóptimo. Puede parecer trivial, pero normalizar su esquema puede ser muy útil.
La clasificación por tipos de caracteres (
text
,varchar
, ...) tiene que ser hecho de acuerdo con la configuración regional - el COTEJO en particular. Lo más probable es que su base de datos use un conjunto local de reglas (como, en mi caso:)de_AT.UTF-8
. Descubre con:Esto hace que la clasificación y las búsquedas de índice sean más lentas . Cuanto más largas sean sus cadenas (nombres de productos), peor. Si en realidad no le interesan las reglas de intercalación en su salida (o el orden de clasificación), esto puede ser más rápido si agrega
COLLATE "C"
:Tenga en cuenta cómo agregué la colación en dos lugares.
El doble de rápido en mi prueba con 20k filas cada una y nombres muy básicos ('good123').
Índice
Si se supone que su consulta debe usar un índice, las columnas con datos de caracteres deben usar una intercalación coincidente (
good
en el ejemplo):Asegúrese de leer los últimos dos capítulos de esta respuesta relacionada en SO:
Incluso puede tener múltiples índices con diferentes clasificaciones en las mismas columnas, si también necesita productos ordenados de acuerdo con otra clasificación (o la predeterminada) en otras consultas.
Normalizar
Las cadenas redundantes (nombre del bien) también hinchan sus tablas e índices, lo que hace que todo sea aún más lento. Con un diseño de tabla adecuado, puede evitar la mayor parte del problema para empezar. Podría verse así:
Las claves principales proporcionan automáticamente (casi) todos los índices que necesitamos.
Según los detalles faltantes, un índice de
price
varias columnas con orden descendente en la segunda columna puede mejorar el rendimiento:Nuevamente, la clasificación debe coincidir con su consulta (ver arriba).
En Postgres 9.2 o posterior, los "índices de cobertura" para escaneos de solo índice podrían ayudar un poco más, especialmente si sus tablas contienen columnas adicionales, lo que hace que la tabla sea sustancialmente más grande que el índice de cobertura.
Estas consultas resultantes son mucho más rápidas:
NO EXISTE
DISTINTO EN
SQL Fiddle.
Soluciones más rápidas
Si eso todavía no es lo suficientemente rápido, puede haber soluciones más rápidas.
CTE recursiva /
JOIN LATERAL
/ subconsulta correlacionadaEspecialmente para distribuciones de datos con muchos precios por bien :
Vista materializada
Si necesita ejecutar esto con frecuencia y rapidez, le sugiero que cree una vista materializada. Creo que es seguro asumir que los precios e inventarios de fechas pasadas rara vez cambian. Calcule el resultado una vez y almacene una instantánea como vista materializada.
Postgres 9.3+ tiene soporte automatizado para vistas materializadas. Puede implementar fácilmente una versión básica en versiones anteriores.
fuente
price_good_date_desc_idx
índice que recomienda mejoró dramáticamente el rendimiento para una consulta mía similar. Mi plan de consulta pasó de un costo de42374.01..42374.86
abajo a0.00..37.12
!Para su información, usé mssql 2008, por lo que Postgres no tendrá el índice "incluir". Sin embargo, el uso de la indexación básica que se muestra a continuación cambiará de combinaciones hash para combinar combinaciones en Postgres: http://explain.depesz.com/s/eF6 (sin índice) http://explain.depesz.com/s/j9x ( con índice en criterios de unión)
Propongo dividir su consulta en dos partes. Primero, una vista (no destinada a mejorar el rendimiento) que se puede usar en una variedad de otros contextos que representa la relación de las fechas de inventario y las fechas de fijación de precios.
Entonces, su consulta puede volverse más simple y fácil de manipular para otros tipos si la consulta (como el uso de combinaciones izquierdas para encontrar inventario sin fechas de precios recientes):
Esto produce el siguiente plan de ejecución: http://sqlfiddle.com/#!3/24f23/1
... Todos los escaneos con un tipo completo. Observe que el costo de rendimiento de las coincidencias hash ocupa la mayor parte del costo total ... y sabemos que los escaneos y la clasificación de la tabla son lentos (en comparación con el objetivo: búsquedas de índice).
Ahora, agregue índices básicos para ayudar a los criterios utilizados en su unión (no pretendo que sean índices óptimos, pero ilustran el punto): http://sqlfiddle.com/#!3/5ec75/1
Esto muestra una mejora. Las operaciones de bucle anidado (unión interna) ya no toman ningún costo total relevante para la consulta. El resto del costo ahora se distribuye entre las búsquedas de índice (un escaneo de inventario porque estamos tirando de cada fila de inventario). Pero aún podemos hacerlo mejor porque la consulta extrae cantidad y precio. Para obtener esos datos, después de evaluar los criterios de unión, se deben realizar búsquedas.
La iteración final usa "incluir" en los índices para facilitar que el plan se desplace y obtenga los datos adicionales solicitados directamente del índice. Entonces las búsquedas se han ido: http://sqlfiddle.com/#!3/5f143/1
Ahora tenemos un plan de consulta donde el costo total de la consulta se distribuye de manera uniforme entre las operaciones de búsqueda de índice muy rápidas. Esto estará cerca de lo mejor posible. Seguramente otros expertos pueden mejorar esto aún más, pero la solución aclara un par de preocupaciones importantes:
fuente
Si tiene PostgreSQL 9.3 (lanzado hoy), puede usar una LATERAL JOIN.
No tengo forma de probar esto, y nunca lo he usado antes, pero por lo que puedo decir de la documentación, la sintaxis sería algo así como:
Esto es básicamente equivalente a la APLICACIÓN de SQL Server , y hay un ejemplo de esto en SQL-Fiddle para fines de demostración.
fuente
Como Erwin y otros han señalado, una consulta eficiente depende de muchas variables y PostgreSQL se esfuerza mucho por optimizar la ejecución de la consulta en función de esas variables. En general, desea escribir para mayor claridad primero y luego modificar para obtener un rendimiento posterior a medida que identifica los cuellos de botella.
Además, PostgreSQL tiene muchos trucos que puede usar para hacer que las cosas sean un poco más eficientes (índices parciales para uno), por lo que, dependiendo de su carga de lectura / escritura, es posible que pueda optimizar esto muy lejos al buscar una indexación cuidadosa.
Lo primero que debe intentar es hacer una vista y unirse a ella:
Esto debería funcionar bien al hacer algo como:
Entonces puedes unirte a eso. La consulta terminará uniendo la vista contra la tabla subyacente, pero suponiendo que tenga un índice único el (fecha, bueno en ese orden ), debería estar listo (ya que esto será una simple búsqueda de caché). Esto funcionará muy bien con algunas filas buscadas, pero será muy ineficiente si está tratando de digerir millones de precios de bienes.
La segunda cosa que puede hacer es agregar a la tabla de inventario una columna bool más reciente y
A continuación, desearía utilizar desencadenantes para establecer most_recent en falso cuando se inserta una nueva fila para un bien. Esto agrega más complejidad y mayores posibilidades de errores, pero es útil.
Nuevamente, mucho de esto depende de los índices apropiados que estén en su lugar. Para las consultas de fechas más recientes, probablemente debería tener un índice de fecha y, posiblemente, uno de varias columnas que comience con la fecha e incluya sus criterios de unión.
Actualice el comentario de Per Erwin a continuación, parece que entendí mal esto. Volviendo a leer la pregunta, no estoy seguro de qué se está haciendo. Quiero mencionar en la actualización cuál es el problema potencial que veo y por qué esto deja esto en claro.
El diseño de la base de datos ofrecido no tiene un uso real de IME con ERP y sistemas de contabilidad. Funcionaría en un modelo hipotético de precios perfectos donde todo lo vendido en un día determinado de un producto determinado tiene el mismo precio. Sin embargo, este no es siempre el caso. Ni siquiera es el caso de cosas como los cambios de divisas (aunque algunos modelos pretenden que sí). Si este es un ejemplo artificial, no está claro. Si es un ejemplo real, existen mayores problemas con el diseño a nivel de datos. Voy a suponer aquí que este es un ejemplo real.
No puede suponer que la fecha sola especifica el precio de un bien determinado. Los precios en cualquier negocio se pueden negociar por contraparte e incluso a veces por transacción. Por esta razón, realmente debe almacenar el precio en la tabla que realmente maneja el inventario dentro o fuera (la tabla de inventario). En tal caso, su tabla de fecha / bienes / precio simplemente especifica un precio base que puede estar sujeto a cambios en función de la negociación. En tal caso, este problema pasa de ser un problema de informes a uno que es transaccional y opera en una fila de cada tabla a la vez. Por ejemplo, puede buscar el precio predeterminado para un producto determinado en un día dado como:
Con un índice de precios (bueno, fecha) esto funcionará bien.
Si este es un ejemplo artificial, quizás algo más cercano a lo que está trabajando ayudaría.
fuente
most_recent
enfoque debería funcionar bien para el precio más reciente absolutamente . Sin embargo, parece que el OP necesita el precio más reciente en relación con cada fecha de inventario.Otra forma sería usar la función de ventana
lead()
para obtener el rango de fechas para cada fila en el precio de la tabla y luego usarlobetween
al unir el inventario. De hecho, he usado esto en la vida real, pero principalmente porque esta fue mi primera idea de cómo resolver esto.SqlFiddle
fuente
Use una unión del inventario al precio con condiciones de unión que limitan los registros del tabelp del precio a solo aquellos que están en o antes de la fecha de inventario, luego extraiga la fecha máxima y donde la fecha es la fecha más alta de ese subconjunto
Entonces, para su precio de inventario:
Si el precio de un bien especificado cambió más de una vez en el mismo día, y realmente solo tiene fechas y no horas en estas columnas, es posible que deba aplicar más restricciones en las uniones para seleccionar solo uno de los registros de cambio de precio.
fuente