PostgreSQL: recupera la fila que tiene el valor máximo para una columna


96

Estoy tratando con una tabla de Postgres (llamada "vidas") que contiene registros con columnas para time_stamp, usr_id, transaction_id y lives_remaining. Necesito una consulta que me dé el total más reciente de lives_remaining para cada usr_id

  1. Hay varios usuarios (usr_id distintos)
  2. time_stamp no es un identificador único: a veces, los eventos de usuario (uno por fila en la tabla) ocurrirán con el mismo time_stamp.
  3. trans_id es único solo para rangos de tiempo muy pequeños: con el tiempo se repite
  4. left_lives (para un usuario determinado) puede aumentar y disminuir con el tiempo

ejemplo:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 3    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Como necesitaré acceder a otras columnas de la fila con los datos más recientes para cada usr_id dado, necesito una consulta que dé un resultado como este:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Como se mencionó, cada usr_id puede ganar o perder vidas y, a veces, estos eventos con marca de tiempo ocurren tan cerca que tienen la misma marca de tiempo. Por lo tanto, esta consulta no funcionará:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

En su lugar, necesito usar time_stamp (primero) y trans_id (segundo) para identificar la fila correcta. Luego, también necesito pasar esa información de la subconsulta a la consulta principal que proporcionará los datos para las otras columnas de las filas apropiadas. Esta es la consulta pirateada con la que me puse a trabajar:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Bien, esto funciona, pero no me gusta. Requiere una consulta dentro de una consulta, una autounión, y me parece que podría ser mucho más simple tomando la fila que MAX encontró que tiene la marca de tiempo y trans_id más grandes. La tabla "vidas" tiene decenas de millones de filas para analizar, por lo que me gustaría que esta consulta fuera lo más rápida y eficiente posible. Soy nuevo en RDBM y Postgres en particular, por lo que sé que necesito hacer un uso efectivo de los índices adecuados. Estoy un poco perdido sobre cómo optimizar.

Encontré una discusión similar aquí . ¿Puedo realizar algún tipo de Postgres equivalente a una función analítica de Oracle?

Cualquier consejo sobre cómo acceder a la información de columna relacionada utilizada por una función agregada (como MAX), crear índices y crear mejores consultas sería muy apreciado.

PD: puedes usar lo siguiente para crear mi caso de ejemplo:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh, es posible que no le guste el hecho de que la consulta se auto-une, etc., pero eso está bien en lo que respecta al RDBMS.
vladr

1
Lo que la autounión en realidad terminará traduciéndose es un mapeo de índice simple, donde el SELECT interno (el que tiene MAX) escanea el índice desechando entradas irrelevantes, y donde el SELECT externo simplemente toma el resto de las columnas de la tabla correspondiente al índice reducido.
vladr

Vlad, gracias por los consejos y la explicación. Me ha abierto los ojos sobre cómo comenzar a comprender el funcionamiento interno de la base de datos y cómo optimizar las consultas. Quassnoi, gracias por la gran consulta y el consejo sobre la clave principal; Bill también. Muy útil.
Joshua Berry

¡Gracias por mostrarme cómo obtener MAX BY2 columnas!

Respuestas:


90

En una tabla con 158k filas pseudoaleatorias (usr_id uniformemente distribuido entre 0 y 10k, trans_iduniformemente distribuido entre 0 y 30),

Por costo de consulta, a continuación, me refiero a la estimación de costos del optimizador basado en costos de Postgres (con los xxx_costvalores predeterminados de Postgres ), que es una estimación de función ponderada de los recursos de CPU y E / S requeridos; puede obtener esto activando PgAdminIII y ejecutando "Consulta / Explicación (F7)" en la consulta con "Opciones de consulta / explicación" configuradas en "Analizar"

  • Consulta de Quassnoy tiene una estimación de costos de 745k (!), Y se completa en 1,3 segundos (dado un índice compuesto de ( usr_id, trans_id, time_stamp))
  • La consulta de Bill tiene un costo estimado de 93k y se completa en 2.9 segundos (dado un índice compuesto en ( usr_id, trans_id))
  • Consulta # 1 a continuación tiene un cálculo del coste de 16k, y se completa en 800 ms (dado un índice compuesto sobre ( usr_id, trans_id, time_stamp))
  • Consulta # 2 a continuación tiene un cálculo del coste de 14k, y se completa en 800 ms (dado un índice de función compuesta en ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • esto es específico de Postgres
  • Consulta # 3 a continuación (Postgres 8.4+) tiene un tiempo de cálculo del coste y la finalización comparables a (o mejor que) consulta # 2 (dado un índice compuesto sobre ( usr_id, time_stamp, trans_id)); tiene la ventaja de escanear la livestabla solo una vez y, si aumenta temporalmente (si es necesario) work_mem para acomodar la clasificación en la memoria, será la más rápida de todas las consultas.

Todos los tiempos anteriores incluyen la recuperación del conjunto de resultados completo de 10k filas.

Su objetivo es una estimación de costo mínima y un tiempo mínimo de ejecución de consultas, con énfasis en el costo estimado. La ejecución de la consulta puede depender significativamente de las condiciones del tiempo de ejecución (por ejemplo, si las filas relevantes ya están completamente almacenadas en la memoria caché o no), mientras que la estimación de costos no. Por otro lado, tenga en cuenta que la estimación de costos es exactamente eso, una estimación.

El mejor tiempo de ejecución de la consulta se obtiene cuando se ejecuta en una base de datos dedicada sin carga (por ejemplo, jugando con pgAdminIII en una PC de desarrollo). El tiempo de consulta variará en la producción según la carga real de la máquina / la distribución del acceso a los datos. Cuando una consulta aparece un poco más rápido (<20%) que la otra, pero tiene un costo mucho más alto, generalmente será más prudente elegir la que tenga un tiempo de ejecución más alto pero un costo menor.

Cuando espere que no haya competencia por la memoria en su máquina de producción en el momento en que se ejecuta la consulta (por ejemplo, la caché RDBMS y la caché del sistema de archivos no serán destruidas por consultas concurrentes y / o actividad del sistema de archivos), entonces el tiempo de consulta que obtuvo en modo autónomo (por ejemplo, pgAdminIII en una PC de desarrollo) será representativo. Si hay contención en el sistema de producción, el tiempo de consulta se degradará proporcionalmente al índice de costo estimado, ya que la consulta con el costo más bajo no depende tanto de la caché mientras que la consulta con un costo más alto revisará los mismos datos una y otra vez (activando E / S adicionales en ausencia de una caché estable), por ejemplo:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

No olvide ejecutar ANALYZE livesuna vez después de crear los índices necesarios.


Consulta nº 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Consulta nº 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

Actualización 29/01/2013

Finalmente, a partir de la versión 8.4, Postgres admite la función de ventana, lo que significa que puede escribir algo tan simple y eficiente como:

Consulta n. ° 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

Por un índice compuesto en (usr_id, trans_id, times_tamp), ¿te refieres a algo como "CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp)"? ¿O debería crear tres índices separados para cada columna? Debería seguir con el valor predeterminado de "USING btree", ¿verdad?
Joshua Berry

1
Sí a la primera opción: me refiero a CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp). :) Salud.
vladr

¡Gracias por hacer la comparación de costos vladr! ¡Respuesta muy completa!
Adam

@vladr Acabo de encontrar tu respuesta. Estoy un poco confundido, ya que dice que la consulta 1 tiene un costo de 16k y la consulta 2 un costo de 14k. Pero más abajo en la tabla, dice que la consulta 1 tiene un costo de 5k y la consulta 2 tiene un costo de 50k. Entonces, ¿qué consulta es la preferida para usar? :) gracias
Houman

1
@Kave, la tabla es para un par hipotético de consultas para ilustrar un ejemplo, no las dos consultas del OP. Cambio de nombre para reducir la confusión.
vladr

77

Propondría una versión limpia basada en DISTINCT ON(ver documentos ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
Esta es una respuesta muy breve y sólida. ¡También tiene una buena referencia! Esta debería ser la respuesta aceptada.
Prakhar Agrawal

Esto pareció funcionar para mí en mi aplicación ligeramente diferente donde nada más funcionaría. Definitivamente debería elevarse para mayor visibilidad.
Jim Factor

8

Aquí hay otro método, que no usa subconsultas correlacionadas o GROUP BY. No soy un experto en el ajuste del rendimiento de PostgreSQL, por lo que le sugiero que pruebe tanto esto como las soluciones proporcionadas por otras personas para ver cuál funciona mejor para usted.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Supongo que trans_ides único al menos sobre cualquier valor dado de time_stamp.


4

Me gusta el estilo de la respuesta de Mike Woodhouse en la otra página que mencionaste. Es especialmente conciso cuando lo que se maximiza es solo una columna, en cuyo caso la subconsulta solo puede usar MAX(some_col)y GROUP BYlas otras columnas, pero en su caso tiene una cantidad de 2 partes para maximizar, aún puede hacerlo usando ORDER BYmás en su LIMIT 1lugar (como lo hizo Quassnoi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Encuentro que usar la sintaxis del constructor de filas es WHERE (a, b, c) IN (subquery)bueno porque reduce la cantidad de verborrea necesaria.


3

De hecho, hay una solución hacky para este problema. Supongamos que desea seleccionar el árbol más grande de cada bosque en una región.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Cuando agrupe árboles por bosques, habrá una lista sin clasificar de árboles y tendrá que encontrar el más grande. Lo primero que debe hacer es ordenar las filas por sus tamaños y seleccionar la primera de su lista. Puede parecer ineficaz, pero si tiene millones de filas, será bastante más rápido que las soluciones que incluyen JOINlas WHEREcondiciones y .

Por cierto, tenga en cuenta que ORDER_BYfor array_aggse introduce en Postgresql 9.0


Tienes un error. Debe escribir ORDER BY tree_size.size DESC. Además, para la tarea del autor, el código se verá así: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky

2

Hay una nueva opción en Postgressql 9.5 llamada DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Elimina filas duplicadas y deja solo la primera fila como se define en la cláusula ORDER BY.

ver la documentación oficial


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

La creación de un índice en (usr_id, time_stamp, trans_id)mejorará enormemente esta consulta.

Siempre, siempre debes tener algún tipo de PRIMARY KEYen tus mesas.


0

Creo que tienes un problema importante aquí: no hay un "contador" que aumente de forma monótona para garantizar que una fila determinada haya ocurrido más tarde que otra. Toma este ejemplo:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

No puede determinar a partir de estos datos cuál es la entrada más reciente. ¿Es el segundo o el último? No hay ninguna función sort o max () que pueda aplicar a cualquiera de estos datos para darle la respuesta correcta.

Aumentar la resolución de la marca de tiempo sería de gran ayuda. Dado que el motor de la base de datos serializa las solicitudes, con una resolución suficiente puede garantizar que no haya dos marcas de tiempo iguales.

Alternativamente, use un trans_id que no se renueve durante mucho, mucho tiempo. Tener un trans_id que se transfiere significa que no puede decir (para la misma marca de tiempo) si trans_id 6 es más reciente que trans_id 1 a menos que haga algunos cálculos matemáticos complicados.


Sí, idealmente una columna de secuencia (autoincremento) estaría en orden.
vladr

La suposición anterior fue que para incrementos de tiempo pequeños, trans_id no se renovaría. Estoy de acuerdo en que la tabla necesita un índice primario único, como un trans_id no repetido. (PD: ¡Estoy feliz de tener suficientes puntos de karma / reputación para comentar!)
Joshua Berry

Vlad afirma que trans_id tiene un ciclo bastante corto que cambia con frecuencia. Incluso si considera solo las dos filas del medio de mi tabla (trans_id = 6 y 1), aún no puede saber cuál es la más reciente. Por lo tanto, usar el máximo (trans_id) para una marca de tiempo determinada no funcionará.
Barry Brown

Sí, confío en la garantía del autor de la aplicación de que la tupla (time_stamp, trans_id) es única para un usuario determinado. Si no es el caso, entonces "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." debe convertirse en "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. DONDE ... AGRUPAR POR l1.usr_id, ...
vladr

0

Otra solución que puede resultarle útil.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.