Manera simple de calcular la mediana con MySQL

208

¿Cuál es la forma más simple (y con suerte no demasiado lenta) de calcular la mediana con MySQL? Lo he usado AVG(x)para encontrar la media, pero me cuesta encontrar una manera simple de calcular la mediana. Por ahora, estoy devolviendo todas las filas a PHP, haciendo una ordenación y luego seleccionando la fila central, pero seguramente debe haber alguna forma simple de hacerlo en una sola consulta MySQL.

Datos de ejemplo:

id | val
--------
 1    4
 2    7
 3    2
 4    2
 5    9
 6    8
 7    3

Al ordenar valda 2 2 3 4 7 8 9, entonces la mediana debería ser 4, versus SELECT AVG(val)cuál == 5.

davr
fuente
72
¿Soy el único con náuseas por el hecho de que MySQL no tiene una función para calcular una mediana? Ridículo.
Monica Heddneck
3
MariaDB ya que la versión 10.3 tiene una, vea mariadb.com/kb/en/library/median
berturion

Respuestas:

225

En MariaDB / MySQL:

SELECT AVG(dd.val) as median_val
FROM (
SELECT d.val, @rownum:=@rownum+1 as `row_number`, @total_rows:=@rownum
  FROM data d, (SELECT @rownum:=0) r
  WHERE d.val is NOT NULL
  -- put some where clause here
  ORDER BY d.val
) as dd
WHERE dd.row_number IN ( FLOOR((@total_rows+1)/2), FLOOR((@total_rows+2)/2) );

Steve Cohen señala que después del primer pase, @rownum contendrá el número total de filas. Esto se puede usar para determinar la mediana, por lo que no se necesita un segundo pase o unión.

También AVG(dd.val)y dd.row_number IN(...)se utiliza para producir correctamente una mediana cuando hay un número par de registros. Razonamiento:

SELECT FLOOR((3+1)/2),FLOOR((3+2)/2); -- when total_rows is 3, avg rows 2 and 2
SELECT FLOOR((4+1)/2),FLOOR((4+2)/2); -- when total_rows is 4, avg rows 2 and 3

Finalmente, MariaDB 10.3.3+ contiene una función MEDIANA

velcrow
fuente
44
alguna forma de hacerlo para mostrar valores de grupo? como: place / median para ese lugar ... como select place, median_value de la tabla ... de alguna manera? gracias
saulob
2
@rowNum tendrá el 'conteo total' al final de la ejecución. Así que puedes usar eso si quieres evitar tener que hacer un 'contar todo' nuevamente (que fue mi caso porque mi consulta no fue tan simple)
Ahmed-Anas
La lógica de tener una declaración: (floor ((total_rows + 1) / 2), floor ((total_rows + 2) / 2)) calcular las filas necesarias para la mediana es increíble! No estoy seguro de cómo pensaste en eso, pero es genial. La parte que no sigo es (SELECT @rownum: = 0) r: ¿para qué sirve esto?
Shanemeister
cambie el primero WHERE 1para WHERE d.val IS NOT NULLque excluya NULLfilas para mantener este método alineado con el nativoAVG
chiliNUT
1
Mi valor provino de una combinación de dos tablas, así que tuve que agregar otra subconsulta para asegurarme de que el orden de las filas fuera correcto después de la combinación. La estructura era algo asíselect avg(value) from (select value, row_number from (select a - b as value from a_table join b_table order by value))
Daniel Buckmaster
62

Acabo de encontrar otra respuesta en línea en los comentarios :

Para medianas en casi cualquier SQL:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val))) = (COUNT(*)+1)/2

Asegúrese de que sus columnas estén bien indexadas y que el índice se utilice para filtrar y ordenar. Verifique con los planes de explicación.

select count(*) from table --find the number of rows

Calcule el número de fila "mediana". Tal vez use:median_row = floor(count / 2) .

Luego selecciónelo de la lista:

select val from table order by val asc limit median_row,1

Esto debería devolverle una fila con solo el valor que desea.

Jacob

TheJacobTaylor
fuente
66
@rob ¿puedes ayudar a editar por favor? ¿O debería simplemente inclinarme ante la solución de velcrow? (no estoy seguro de cómo diferir a otra solución) Gracias, Jacob
TheJacobTaylor
1
Tenga en cuenta que hace una "unión cruzada", que es muy lenta para tablas grandes.
Rick James
1
Esta respuesta no devuelve nada para un número par de filas.
kuttumiah
Esta respuesta no funciona en absoluto para algunos conjuntos de datos, por ejemplo, el conjunto de datos trivial con valores 0.1, 0.1, 0.1, 2: funcionará si todos los valores son distintos, pero solo funciona si los valores
Kem Mason
32

Descubrí que la solución aceptada no funcionaba en mi instalación de MySQL, devolviendo un conjunto vacío, pero esta consulta funcionó para mí en todas las situaciones en las que la probé:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val)))/COUNT(*) > .5
LIMIT 1
zookatron
fuente
1
absolutamente correcto, funciona perfectamente y muy rápido en mis tablas indexadas
Rob
2
esta parece ser la solución más rápida en mysql de todas las respuestas aquí, 200 ms con poco menos de un millón de registros en la tabla
Rob
3
@FrankConijn: selecciona de una tabla dos veces. El nombre de la tabla es datay se está utilizando con dos nombres, xy y.
Brian
3
solo digo que detuve mi mysqld con esta consulta exacta en una tabla con 33k filas ...
Xenonite
1
Esta consulta devuelve una respuesta incorrecta para un número par de filas.
kuttumiah
26

Desafortunadamente, ni las respuestas de TheJacobTaylor ni Velcrow devuelven resultados precisos para las versiones actuales de MySQL.

La respuesta de Velcro desde arriba es cercana, pero no se calcula correctamente para conjuntos de resultados con un número par de filas. Las medianas se definen como 1) el número del medio en conjuntos con números impares, o 2) el promedio de los dos números del medio en conjuntos de números pares.

Entonces, aquí está la solución de velcro parcheada para manejar conjuntos de números pares e impares:

SELECT AVG(middle_values) AS 'median' FROM (
  SELECT t1.median_column AS 'middle_values' FROM
    (
      SELECT @row:=@row+1 as `row`, x.median_column
      FROM median_table AS x, (SELECT @row:=0) AS r
      WHERE 1
      -- put some where clause here
      ORDER BY x.median_column
    ) AS t1,
    (
      SELECT COUNT(*) as 'count'
      FROM median_table x
      WHERE 1
      -- put same where clause here
    ) AS t2
    -- the following condition will return 1 record for odd number sets, or 2 records for even number sets.
    WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;

Para usar esto, siga estos 3 sencillos pasos:

  1. Reemplace "median_table" (2 veces) en el código anterior con el nombre de su tabla
  2. Reemplace "median_column" (3 veces) con el nombre de la columna para la que desea encontrar una mediana
  3. Si tiene una condición WHERE, reemplace "WHERE 1" (2 veces) con su condición where
Beto
fuente
Y, ¿qué haces para la mediana de los valores de cadena?
Rick James
12

Propongo una forma más rápida.

Obtenga el recuento de filas:

SELECT CEIL(COUNT(*)/2) FROM data;

Luego tome el valor medio en una subconsulta ordenada:

SELECT max(val) FROM (SELECT val FROM data ORDER BY val limit @middlevalue) x;

Probé esto con un conjunto de datos de 5x10e6 de números aleatorios y encontrará la mediana en menos de 10 segundos.

Reggie Edwards
fuente
3
¿Por qué no? SELECCIONE val FROM data ORDER BY val limit @middlevalue, 1
Bryan
1
¿Cómo se extrae la salida variable de su primer bloque de código en su segundo bloque de código?
Viaje
3
Como en, ¿de dónde viene @middlevalue?
Viaje del
@Bryan: estoy de acuerdo contigo, eso tiene mucho más sentido para mí. ¿Alguna vez encontraste una razón para no hacerlo de esa manera?
Shane N
55
Esto no funciona ya que una variable no se puede usar en la cláusula límite.
codepk
8

Un comentario en esta página en la documentación de MySQL tiene la siguiente sugerencia:

-- (mostly) High Performance scaling MEDIAN function per group
-- Median defined in http://en.wikipedia.org/wiki/Median
--
-- by Peter Hlavac
-- 06.11.2008
--
-- Example Table:

DROP table if exists table_median;
CREATE TABLE table_median (id INTEGER(11),val INTEGER(11));
COMMIT;


INSERT INTO table_median (id, val) VALUES
(1, 7), (1, 4), (1, 5), (1, 1), (1, 8), (1, 3), (1, 6),
(2, 4),
(3, 5), (3, 2),
(4, 5), (4, 12), (4, 1), (4, 7);



-- Calculating the MEDIAN
SELECT @a := 0;
SELECT
id,
AVG(val) AS MEDIAN
FROM (
SELECT
id,
val
FROM (
SELECT
-- Create an index n for every id
@a := (@a + 1) mod o.c AS shifted_n,
IF(@a mod o.c=0, o.c, @a) AS n,
o.id,
o.val,
-- the number of elements for every id
o.c
FROM (
SELECT
t_o.id,
val,
c
FROM
table_median t_o INNER JOIN
(SELECT
id,
COUNT(1) AS c
FROM
table_median
GROUP BY
id
) t2
ON (t2.id = t_o.id)
ORDER BY
t_o.id,val
) o
) a
WHERE
IF(
-- if there is an even number of elements
-- take the lower and the upper median
-- and use AVG(lower,upper)
c MOD 2 = 0,
n = c DIV 2 OR n = (c DIV 2)+1,

-- if its an odd number of elements
-- take the first if its only one element
-- or take the one in the middle
IF(
c = 1,
n = 1,
n = c DIV 2 + 1
)
)
) a
GROUP BY
id;

-- Explanation:
-- The Statement creates a helper table like
--
-- n id val count
-- ----------------
-- 1, 1, 1, 7
-- 2, 1, 3, 7
-- 3, 1, 4, 7
-- 4, 1, 5, 7
-- 5, 1, 6, 7
-- 6, 1, 7, 7
-- 7, 1, 8, 7
--
-- 1, 2, 4, 1

-- 1, 3, 2, 2
-- 2, 3, 5, 2
--
-- 1, 4, 1, 4
-- 2, 4, 5, 4
-- 3, 4, 7, 4
-- 4, 4, 12, 4


-- from there we can select the n-th element on the position: count div 2 + 1 
Sebastian Paaske Tørholm
fuente
En mi humilde opinión, éste es claramente el mejor para situaciones donde se necesita la mediana de una complicada subconjunto (s) (que necesitaba para calcular las medianas separadas de un gran número de subconjuntos de datos)
mblackwell8
Funciona bien para mi. 5.6.14 Servidor de comunidad MySQL. La tabla con registros de 11M (aproximadamente 20 Gb en disco) tiene dos índices no primarios (model_id, precio). En la tabla (después de la filtración) tenemos 500K registros para calcular la mediana. Como resultado tenemos 30K registros (model_id, median_price). La duración de la consulta es de 1.5-2 segundos. La velocidad es rápida para mí.
Mikl
8

Instale y use estas funciones estadísticas de mysql: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

Después de eso, calcular la mediana es fácil:

SELECT median(val) FROM data;
Leonardo Nicolas
fuente
1
Acabo de probar esto yo mismo, y por lo que vale, instalarlo fue súper rápido / fácil, y funcionó como se anuncia, incluida la agrupación, por ejemplo, "seleccionar nombre, mediana (x) DEL grupo t1 por nombre" - fuente de github aquí: github.com/infusion/udf_infusion
Kem Mason el
6

La mayoría de las soluciones anteriores funcionan solo para un campo de la tabla, es posible que deba obtener la mediana (percentil 50) para muchos campos de la consulta.

Yo uso esto:

SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(
 GROUP_CONCAT(field_name ORDER BY field_name SEPARATOR ','),
  ',', 50/100 * COUNT(*) + 1), ',', -1) AS DECIMAL) AS `Median`
FROM table_name;

Puede reemplazar el "50" en el ejemplo anterior a cualquier percentil, es muy eficiente.

Solo asegúrese de tener suficiente memoria para GROUP_CONCAT, puede cambiarlo con:

SET group_concat_max_len = 10485760; #10MB max length

Más detalles: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/

Nico
fuente
Tenga en cuenta: para un número par de valores, toma el mayor de los dos valores medios. Para el número de probabilidades de los valores, toma el siguiente valor más alto después de la mediana.
giordano
6

Tengo el siguiente código que encontré en HackerRank y es bastante simple y funciona en todos y cada uno de los casos.

SELECT M.MEDIAN_COL FROM MEDIAN_TABLE M WHERE  
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL < M.MEDIAN_COL ) = 
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL > M.MEDIAN_COL );
Prashant Srivastav
fuente
2
Creo que esto solo funciona con una tabla que tiene el número de entradas es impar. Para un número par de entradas, esto puede tener un problema.
Y. Chang
4

Partiendo de la respuesta de velcro, para aquellos de ustedes que tienen que hacer una mediana de algo que está agrupado por otro parámetro:

SELECCIONE grp_field , t1 . val FROM ( SELECCIONE grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ 
         row_number
    s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   FROM datos d , (    SELECT @ rownum : = 0 , @ s : = 0 ) r
   ORDEN POR grp_field , d . val
 ) como t1 JOIN ( SELECCIONE grp_field , count (*) como total_rows
   FROM data d
   GROUP BY grp_field
 ) como t2
 ON t1 . grp_field = t2 . grp_field
 DONDE t1 . numero de fila     
     = piso( total_rows / 2 ) +1 ;

Doug
fuente
3

Puede usar la función definida por el usuario que se encuentra aquí .

Alex Martelli
fuente
3
Esto parece lo más útil, pero no quiero instalar un software alfa inestable que pueda causar que mysql se bloquee en mi servidor de producción :(
davr
66
Por lo tanto, estudie sus fuentes para la función de interés, corríjalas o modifíquelas según sea necesario, e instale "su propia" versión estable y no alfa una vez que lo haya hecho. entraste en SO? -)
Alex Martelli
3

Se preocupa por un recuento de valores impares; en ese caso, da el promedio de los dos valores en el medio.

SELECT AVG(val) FROM
  ( SELECT x.id, x.val from data x, data y
      GROUP BY x.id, x.val
      HAVING SUM(SIGN(1-SIGN(IF(y.val-x.val=0 AND x.id != y.id, SIGN(x.id-y.id), y.val-x.val)))) IN (ROUND((COUNT(*))/2), ROUND((COUNT(*)+1)/2))
  ) sq
Franz K.
fuente
2

Mi código, eficiente sin tablas o variables adicionales:

SELECT
((SUBSTRING_INDEX(SUBSTRING_INDEX(group_concat(val order by val), ',', floor(1+((count(val)-1) / 2))), ',', -1))
+
(SUBSTRING_INDEX(SUBSTRING_INDEX(group_concat(val order by val), ',', ceiling(1+((count(val)-1) / 2))), ',', -1)))/2
as median
FROM table;
Oscar Canon
fuente
3
Esto fallará en cualquier cantidad sustancial de datos porque GROUP_CONCATestá limitado a 1023 caracteres, incluso cuando se usa dentro de otra función como esta.
Rob Van Dam
2

Opcionalmente, también puede hacer esto en un procedimiento almacenado:

DROP PROCEDURE IF EXISTS median;
DELIMITER //
CREATE PROCEDURE median (table_name VARCHAR(255), column_name VARCHAR(255), where_clause VARCHAR(255))
BEGIN
  -- Set default parameters
  IF where_clause IS NULL OR where_clause = '' THEN
    SET where_clause = 1;
  END IF;

  -- Prepare statement
  SET @sql = CONCAT(
    "SELECT AVG(middle_values) AS 'median' FROM (
      SELECT t1.", column_name, " AS 'middle_values' FROM
        (
          SELECT @row:=@row+1 as `row`, x.", column_name, "
          FROM ", table_name," AS x, (SELECT @row:=0) AS r
          WHERE ", where_clause, " ORDER BY x.", column_name, "
        ) AS t1,
        (
          SELECT COUNT(*) as 'count'
          FROM ", table_name, " x
          WHERE ", where_clause, "
        ) AS t2
        -- the following condition will return 1 record for odd number sets, or 2 records for even number sets.
        WHERE t1.row >= t2.count/2
          AND t1.row <= ((t2.count/2)+1)) AS t3
    ");

  -- Execute statement
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
END//
DELIMITER ;


-- Sample usage:
-- median(table_name, column_name, where_condition);
CALL median('products', 'price', NULL);
Beto
fuente
¡Gracias por esto! El usuario debe tener en cuenta que los valores faltantes (NULL) se consideran valores. para evitar este problema agregue 'x NO ES NULO donde condición.
giordano
1
@ giordano ¿En qué línea del código se x IS NOT NULLdebe agregar?
Przemyslaw Remin
1
@PrzemyslawRemin Lo siento, no estaba claro en mi declaración y me di cuenta ahora de que el SP ya considera el caso de valores perdidos. El SP debe ser llamado de esta manera: CALL median("table","x","x IS NOT NULL").
giordano
2

Mi solución presentada a continuación funciona en una sola consulta sin creación de tabla, variable o incluso subconsulta. Además, le permite obtener una mediana para cada grupo en consultas grupales (¡esto es lo que necesitaba!):

SELECT `columnA`, 
SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(`columnB` ORDER BY `columnB`), ',', CEILING((COUNT(`columnB`)/2))), ',', -1) medianOfColumnB
FROM `tableC`
-- some where clause if you want
GROUP BY `columnA`;

Funciona debido al uso inteligente de group_concat y substring_index.

Pero, para permitir big group_concat, debe establecer group_concat_max_len en un valor más alto (1024 caracteres por defecto). Puede configurarlo así (para la sesión SQL actual):

SET SESSION group_concat_max_len = 10000; 
-- up to 4294967295 in 32-bits platform.

Más información para group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len

didier2l
fuente
2

Otro riff en la respuesta de Velcrow, pero usa una sola tabla intermedia y aprovecha la variable utilizada para la numeración de filas para obtener el recuento, en lugar de realizar una consulta adicional para calcularlo. También comienza el recuento para que la primera fila sea la fila 0 para permitir simplemente usar Floor y Ceil para seleccionar la (s) fila (s) mediana (s).

SELECT Avg(tmp.val) as median_val
    FROM (SELECT inTab.val, @rows := @rows + 1 as rowNum
              FROM data as inTab,  (SELECT @rows := -1) as init
              -- Replace with better where clause or delete
              WHERE 2 > 1
              ORDER BY inTab.val) as tmp
    WHERE tmp.rowNum in (Floor(@rows / 2), Ceil(@rows / 2));
Steve Cohen
fuente
2
SELECT 
    SUBSTRING_INDEX(
        SUBSTRING_INDEX(
            GROUP_CONCAT(field ORDER BY field),
            ',',
            ((
                ROUND(
                    LENGTH(GROUP_CONCAT(field)) - 
                    LENGTH(
                        REPLACE(
                            GROUP_CONCAT(field),
                            ',',
                            ''
                        )
                    )
                ) / 2) + 1
            )),
            ',',
            -1
        )
FROM
    table

Lo anterior parece funcionar para mí.

Nochum Sossonko
fuente
No está devolviendo la mediana correcta para un número par de valores. Por ejemplo, la mediana de {98,102,102,98}es 100pero su código da 102. Funcionó bien para números impares.
Nomiluks
1

Usé un enfoque de dos consultas:

  • el primero en contar, min, max y avg
  • segundo (declaración preparada) con las cláusulas "LIMIT @ count / 2, 1" y "ORDER BY .." para obtener el valor medio

Estos se envuelven en una función defn, por lo que todos los valores se pueden devolver de una llamada.

Si sus rangos son estáticos y sus datos no cambian con frecuencia, podría ser más eficiente calcular previamente / almacenar estos valores y utilizar los valores almacenados en lugar de consultar desde cero cada vez.

btk
fuente
1

Como solo necesitaba una solución mediana Y percentil, hice una función simple y bastante flexible basada en los hallazgos de este hilo. Sé que estoy feliz si encuentro funciones "listas para usar" que son fáciles de incluir en mis proyectos, así que decidí compartir rápidamente:

function mysql_percentile($table, $column, $where, $percentile = 0.5) {

    $sql = "
            SELECT `t1`.`".$column."` as `percentile` FROM (
            SELECT @rownum:=@rownum+1 as `row_number`, `d`.`".$column."`
              FROM `".$table."` `d`,  (SELECT @rownum:=0) `r`
              ".$where."
              ORDER BY `d`.`".$column."`
            ) as `t1`, 
            (
              SELECT count(*) as `total_rows`
              FROM `".$table."` `d`
              ".$where."
            ) as `t2`
            WHERE 1
            AND `t1`.`row_number`=floor(`total_rows` * ".$percentile.")+1;
        ";

    $result = sql($sql, 1);

    if (!empty($result)) {
        return $result['percentile'];       
    } else {
        return 0;
    }

}

El uso es muy fácil, ejemplo de mi proyecto actual:

...
$table = DBPRE."zip_".$slug;
$column = 'seconds';
$where = "WHERE `reached` = '1' AND `time` >= '".$start_time."'";

    $reaching['median'] = mysql_percentile($table, $column, $where, 0.5);
    $reaching['percentile25'] = mysql_percentile($table, $column, $where, 0.25);
    $reaching['percentile75'] = mysql_percentile($table, $column, $where, 0.75);
...
bezoo
fuente
1

Aquí está mi camino. Por supuesto, podría ponerlo en un procedimiento :-)

SET @median_counter = (SELECT FLOOR(COUNT(*)/2) - 1 AS `median_counter` FROM `data`);

SET @median = CONCAT('SELECT `val` FROM `data` ORDER BY `val` LIMIT ', @median_counter, ', 1');

PREPARE median FROM @median;

EXECUTE median;

Puede evitar la variable @median_counter, si la sustituye:

SET @median = CONCAT( 'SELECT `val` FROM `data` ORDER BY `val` LIMIT ',
                      (SELECT FLOOR(COUNT(*)/2) - 1 AS `median_counter` FROM `data`),
                      ', 1'
                    );

PREPARE median FROM @median;

EXECUTE median;
pucawo
fuente
1

Esta manera parece incluir recuento par e impar sin subconsulta.

SELECT AVG(t1.x)
FROM table t1, table t2
GROUP BY t1.x
HAVING SUM(SIGN(t1.x - t2.x)) = 0
Yuhanluo
fuente
¿Podría decir cuál es la tabla t2?
xliiv hace
1

Basado en la respuesta de @ bob, esto generaliza la consulta para tener la capacidad de devolver múltiples medianas, agrupadas según algunos criterios.

Piense, por ejemplo, el precio medio de venta de automóviles usados ​​en un lote de automóviles, agrupados por año-mes.

SELECT 
    period, 
    AVG(middle_values) AS 'median' 
FROM (
    SELECT t1.sale_price AS 'middle_values', t1.row_num, t1.period, t2.count
    FROM (
        SELECT 
            @last_period:=@period AS 'last_period',
            @period:=DATE_FORMAT(sale_date, '%Y-%m') AS 'period',
            IF (@period<>@last_period, @row:=1, @row:=@row+1) as `row_num`, 
            x.sale_price
          FROM listings AS x, (SELECT @row:=0) AS r
          WHERE 1
            -- where criteria goes here
          ORDER BY DATE_FORMAT(sale_date, '%Y%m'), x.sale_price
        ) AS t1
    LEFT JOIN (  
          SELECT COUNT(*) as 'count', DATE_FORMAT(sale_date, '%Y-%m') AS 'period'
          FROM listings x
          WHERE 1
            -- same where criteria goes here
          GROUP BY DATE_FORMAT(sale_date, '%Y%m')
        ) AS t2
        ON t1.period = t2.period
    ) AS t3
WHERE 
    row_num >= (count/2) 
    AND row_num <= ((count/2) + 1)
GROUP BY t3.period
ORDER BY t3.period;
Ariel Allon
fuente
1

A menudo, es posible que necesitemos calcular la mediana no solo para toda la tabla, sino también para los agregados con respecto a nuestra identificación. En otras palabras, calcule la mediana de cada ID en nuestra tabla, donde cada ID tiene muchos registros. (buen rendimiento y funciona en muchos SQL + corrige el problema de pares e impares, más sobre el rendimiento de diferentes métodos Medianos https://sqlperformance.com/2012/08/t-sql-queries/median )

SELECT our_id, AVG(1.0 * our_val) as Median
FROM
( SELECT our_id, our_val, 
  COUNT(*) OVER (PARTITION BY our_id) AS cnt,
  ROW_NUMBER() OVER (PARTITION BY our_id ORDER BY our_val) AS rn
  FROM our_table
) AS x
WHERE rn IN ((cnt + 1)/2, (cnt + 2)/2) GROUP BY our_id;

Espero eso ayude

Danylo Zherebetskyy
fuente
Es la mejor solución. Sin embargo, para conjuntos de datos grandes se ralentizará porque vuelve a contar para cada elemento en cada conjunto. Para hacerlo más rápido, ponga "COUNT (*)" para separar la subconsulta.
Slava Murygin
1

MySQL ha admitido funciones de ventana desde la versión 8.0, puede usar ROW_NUMBERo DENSE_RANK( NO use, RANKya que asigna el mismo rango a los mismos valores, como en el ranking deportivo):

SELECT AVG(t1.val) AS median_val
  FROM (SELECT val, 
               ROW_NUMBER() OVER(ORDER BY val) AS rownum
          FROM data) t1,
       (SELECT COUNT(*) AS num_records FROM data) t2
 WHERE t1.row_num IN
       (FLOOR((t2.num_records + 1) / 2), 
        FLOOR((t2.num_records + 2) / 2));
rhanqtl
fuente
0

Si MySQL tiene ROW_NUMBER, entonces MEDIAN es (inspírese en esta consulta de SQL Server):

WITH Numbered AS 
(
SELECT *, COUNT(*) OVER () AS Cnt,
    ROW_NUMBER() OVER (ORDER BY val) AS RowNum
FROM yourtable
)
SELECT id, val
FROM Numbered
WHERE RowNum IN ((Cnt+1)/2, (Cnt+2)/2)
;

El IN se usa en caso de que tenga un número par de entradas.

Si desea encontrar la mediana por grupo, simplemente PARTICIÓN POR grupo en sus cláusulas OVER.

Robar

Rob Farley
fuente
1
No, no ROW_NUMBER OVER, no PARTICIÓN POR, nada de eso; esto es MySql, no un motor de base de datos real como PostgreSQL, IBM DB2, MS SQL Server, etc. ;-).
Alex Martelli
0

Después de leer todos los anteriores, no coincidían con mi requisito real, por lo que implementé el mío que no necesita ningún procedimiento ni complicar las declaraciones, solo yo GROUP_CONCAT todos los valores de la columna que quería obtener la MEDIANA y aplicando un COUNT DIV BY 2 Extraigo el valor desde el medio de la lista como lo hace la siguiente consulta:

(POS es el nombre de la columna que quiero obtener su mediana)

(query) SELECT
SUBSTRING_INDEX ( 
   SUBSTRING_INDEX ( 
       GROUP_CONCAT(pos ORDER BY CAST(pos AS SIGNED INTEGER) desc SEPARATOR ';') 
    , ';', COUNT(*)/2 ) 
, ';', -1 ) AS `pos_med`
FROM table_name
GROUP BY any_criterial

Espero que esto pueda ser útil para alguien en la forma en que muchos otros comentarios fueron para mí desde este sitio web.

Gabriel G.
fuente
0

Conociendo el recuento exacto de filas, puede usar esta consulta:

SELECT <value> AS VAL FROM <table> ORDER BY VAL LIMIT 1 OFFSET <half>

Dónde <half> = ceiling(<size> / 2.0) - 1

ZhekaKozlov
fuente
0

Tengo una base de datos que contiene aproximadamente mil millones de filas que necesitamos para determinar la edad promedio en el conjunto. Ordenar mil millones de filas es difícil, pero si agrega los distintos valores que se pueden encontrar (las edades oscilan entre 0 y 100), puede ordenar ESTA lista y usar algo de magia aritmética para encontrar el percentil que desee de la siguiente manera:

with rawData(count_value) as
(
    select p.YEAR_OF_BIRTH
        from dbo.PERSON p
),
overallStats (avg_value, stdev_value, min_value, max_value, total) as
(
  select avg(1.0 * count_value) as avg_value,
    stdev(count_value) as stdev_value,
    min(count_value) as min_value,
    max(count_value) as max_value,
    count(*) as total
  from rawData
),
aggData (count_value, total, accumulated) as
(
  select count_value, 
    count(*) as total, 
        SUM(count(*)) OVER (ORDER BY count_value ROWS UNBOUNDED PRECEDING) as accumulated
  FROM rawData
  group by count_value
)
select o.total as count_value,
  o.min_value,
    o.max_value,
    o.avg_value,
    o.stdev_value,
    MIN(case when d.accumulated >= .50 * o.total then count_value else o.max_value end) as median_value,
    MIN(case when d.accumulated >= .10 * o.total then count_value else o.max_value end) as p10_value,
    MIN(case when d.accumulated >= .25 * o.total then count_value else o.max_value end) as p25_value,
    MIN(case when d.accumulated >= .75 * o.total then count_value else o.max_value end) as p75_value,
    MIN(case when d.accumulated >= .90 * o.total then count_value else o.max_value end) as p90_value
from aggData d
cross apply overallStats o
GROUP BY o.total, o.min_value, o.max_value, o.avg_value, o.stdev_value
;

Esta consulta depende de las funciones de la ventana de soporte de db (incluidas las PRECEDENTES SIN LÍMITES DE FILAS), pero si no tiene eso, es simple unir aggData CTE consigo mismo y agregar todos los totales anteriores en la columna 'acumulada' que se utiliza para determinar qué El valor contiene el precentil especificado. La muestra anterior calcula p10, p25, p50 (mediana), p75 y p90.

-Chris

Chris Knoll
fuente
0

Tomado de: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

Sugeriría otra forma, sin unirse , pero trabajando con cadenas

No lo verifiqué con tablas con datos grandes, pero las tablas pequeñas / medianas funcionan bien.

Lo bueno aquí, es que también funciona AGRUPANDO para que pueda devolver la mediana de varios elementos.

Aquí está el código de prueba para la tabla de prueba:

DROP TABLE test.test_median
CREATE TABLE test.test_median AS
SELECT 'book' AS grp, 4 AS val UNION ALL
SELECT 'book', 7 UNION ALL
SELECT 'book', 2 UNION ALL
SELECT 'book', 2 UNION ALL
SELECT 'book', 9 UNION ALL
SELECT 'book', 8 UNION ALL
SELECT 'book', 3 UNION ALL

SELECT 'note', 11 UNION ALL

SELECT 'bike', 22 UNION ALL
SELECT 'bike', 26 

y el código para encontrar la mediana para cada grupo:

SELECT grp,
         SUBSTRING_INDEX( SUBSTRING_INDEX( GROUP_CONCAT(val ORDER BY val), ',', COUNT(*)/2 ), ',', -1) as the_median,
         GROUP_CONCAT(val ORDER BY val) as all_vals_for_debug
FROM test.test_median
GROUP BY grp

Salida:

grp | the_median| all_vals_for_debug
bike| 22        | 22,26
book| 4         | 2,2,3,4,7,8,9
note| 11        | 11
mr.baby123
fuente
¿No crees que la mediana de `{22,26}` debería ser 24?
Nomiluks
0

En algunos casos, la mediana se calcula de la siguiente manera:

La "mediana" es el valor "medio" en la lista de números cuando están ordenados por valor. Para conjuntos de conteo par, la mediana es el promedio de los dos valores medios . He creado un código simple para eso:

$midValue = 0;
$rowCount = "SELECT count(*) as count {$from} {$where}";

$even = FALSE;
$offset = 1;
$medianRow = floor($rowCount / 2);
if ($rowCount % 2 == 0 && !empty($medianRow)) {
  $even = TRUE;
  $offset++;
  $medianRow--;
}

$medianValue = "SELECT column as median 
               {$fromClause} {$whereClause} 
               ORDER BY median 
               LIMIT {$medianRow},{$offset}";

$medianValDAO = db_query($medianValue);
while ($medianValDAO->fetch()) {
  if ($even) {
    $midValue = $midValue + $medianValDAO->median;
  }
  else {
    $median = $medianValDAO->median;
  }
}
if ($even) {
  $median = $midValue / 2;
}
return $median;

La mediana de $ devuelta sería el resultado requerido :-)

jitendrapurohit
fuente