Maneira simples de calcular mediana com MySQL


208

Qual é a maneira mais simples (e espero que não seja muito lenta) de calcular a mediana com o MySQL? Eu usei AVG(x)para encontrar a média, mas estou tendo dificuldades para encontrar uma maneira simples de calcular a mediana. Por enquanto, estou retornando todas as linhas para o PHP, fazendo uma classificação e depois escolhendo a linha do meio, mas certamente deve haver uma maneira simples de fazer isso em uma única consulta do MySQL.

Dados de exemplo:

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

Classificar em val2 2 3 4 7 8 9, então a mediana deve ser 4, versus SELECT AVG(val)qual == 5.


72
Eu sou o único nauseado pelo fato de o MySQL não ter uma função para calcular uma mediana? Ridículo.
Monica Heddneck

3
MariaDB desde a versão 10.3 tem um, ver mariadb.com/kb/en/library/median
berturion

Respostas:


225

No 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 ressalta que, após a primeira passagem, o @rownum conterá o número total de linhas. Isso pode ser usado para determinar a mediana, portanto, não é necessária nenhuma segunda passagem ou junção.

Também AVG(dd.val)e dd.row_number IN(...)é usado para produzir corretamente uma mediana quando há um número par de registros. Raciocínio:

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, o MariaDB 10.3.3+ contém uma função MEDIAN


4
alguma maneira de mostrar valores de grupo? como: local / mediana para esse local ... como selecionar local, median_value da tabela ... de alguma forma? obrigado
saulob 18/01/14

2
O @rowNum terá a 'contagem total' no final da execução. Então você pode usar isso se você quer evitar ter que fazer um 'contar tudo' novamente (que foi o meu caso porque minha consulta não era tão simples)
Ahmed-Anas

A lógica de ter uma declaração: (floor ((total_rows + 1) / 2), floor ((total_rows + 2) / 2)) calcular as linhas necessárias para a mediana é impressionante! Não sei como você pensou nisso, mas é brilhante. A parte que não sigo é a (SELECT @rownum: = 0) r - a que finalidade isso serve?
21817 Shanemister

altere o primeiro WHERE 1para WHERE d.val IS NOT NULLpara que exclua NULLlinhas para manter esse método alinhado com o nativoAVG
chiliNUT

1
Meu valor veio de uma junção de duas tabelas, então tive que adicionar outra subconsulta para garantir que a ordem das linhas estivesse correta após a junção! A estrutura era meio queselect 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

Acabei de encontrar outra resposta online nos comentários :

Para medianas em quase qualquer 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

Verifique se suas colunas estão bem indexadas e se o índice é usado para filtragem e classificação. Verifique com os planos de explicação.

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

Calcule o número da linha "mediana". Talvez usar: median_row = floor(count / 2).

Em seguida, escolha-o na lista:

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

Isso deve retornar uma linha com apenas o valor desejado.

Jacob


6
@rob você pode ajudar a editar por favor? Ou devo apenas me curvar diante da solução de velcrow? (não realmente certo de como adiar para outra solução) Obrigado, Jacob
TheJacobTaylor

1
Observe que ele faz uma "junção cruzada", o que é muito lento para tabelas grandes.
Rick James

1
Esta resposta não retorna nada para o número par de linhas.
precisa saber é

Esta resposta não funciona de modo algum para alguns conjuntos de dados, por exemplo, o conjunto de dados triviais com valores 0.1, 0.1, 0.1, 2 - funcionará se todos os valores forem distintos, mas só funcionará se os valores
Kem Mason

32

Descobri que a solução aceita não funcionava na minha instalação do MySQL, retornando um conjunto vazio, mas essa consulta funcionou para mim em todas as situações em que a testei:

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

1
absolutamente correto, funciona perfeitamente e muito rápida em minhas tabelas indexados
Rob

2
esta parece ser a solução mais rápida no mysql de todas as respostas aqui, 200ms com pouco menos de um milhão de registros na tabela
Rob

3
@FrankConijn: Ele seleciona uma tabela duas vezes. O nome da tabela é datae está sendo usado com dois nomes, xe y.
Brian

3
apenas dizendo i parado meu mysqld com essa consulta exata em uma tabela com 33k linhas ...
Xenonite

1
Esta consulta retorna uma resposta incorreta para o número par de linhas.
precisa saber é

26

Infelizmente, nem as respostas de TheJacobTaylor nem de velcrow retornam resultados precisos para as versões atuais do MySQL.

A resposta do Velcro acima é aproximada, mas não calcula corretamente os conjuntos de resultados com um número par de linhas. As medianas são definidas como 1) o número do meio em conjuntos de números ímpares ou 2) a média dos dois números do meio em conjuntos de números pares.

Então, aqui está a solução do velcro corrigida para lidar com conjuntos de números pares e ímpares:

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 isso, siga estas 3 etapas fáceis:

  1. Substitua "median_table" (2 ocorrências) no código acima pelo nome da sua tabela
  2. Substitua "median_column" (3 ocorrências) pelo nome da coluna para a qual você deseja encontrar uma mediana
  3. Se você tiver uma condição WHERE, substitua "WHERE 1" (2 ocorrências) pela sua condição where

E o que você faz pela mediana dos valores das strings?
Rick James

12

Eu proponho uma maneira mais rápida.

Obtenha a contagem de linhas:

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

Em seguida, pegue o valor do meio em uma subconsulta classificada:

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

Testei isso com um conjunto de dados 5x10e6 de números aleatórios e ele encontrará a mediana em menos de 10 segundos.


3
Por que não: SELECT val FROM data ORDER BY val limit @middlevalue, 1
Bryan

1
Como você puxa a saída variável do seu primeiro bloco de código para o seu segundo bloco de código?
Viagem

3
Como, de onde vem o @middlevalue?
Viagem

@ Bryan - eu concordo com você, isso faz muito mais sentido para mim. Você já encontrou um motivo para não fazer dessa maneira?
Shane N

5
Isso não funciona, pois uma variável não pode ser usada na cláusula de limite.
codepk

8

Um comentário nesta página na documentação do MySQL tem a seguinte sugestão:

-- (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 

IMHO, este é claramente o melhor para situações onde você precisa a mediana de um subconjunto complicado (s) (eu precisava para calcular médias separadas de um grande número de subconjuntos de dados)
mblackwell8

Funciona bem para mim. 5.6.14 MySQL Community Server. A tabela com 11 milhões de registros (cerca de 20 GB no disco), possui dois índices não primários (model_id, price). Na tabela (após a filtragem), temos 500 mil registros para calcular a mediana. Como resultado, temos 30 mil registros (model_id, median_price). A duração da consulta é de 1,5 a 2 segundos. A velocidade é rápida para mim.
Mikl


6

A maioria das soluções acima funciona apenas para um campo da tabela; talvez seja necessário obter a mediana (percentil 50) de muitos campos na consulta.

Eu uso isso:

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;

Você pode substituir o "50" no exemplo acima por qualquer percentil, é muito eficiente.

Apenas verifique se você tem memória suficiente para o GROUP_CONCAT, você pode alterá-lo com:

SET group_concat_max_len = 10485760; #10MB max length

Mais detalhes: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/


Esteja ciente: para um número par de valores, leva o maior dos dois valores médios. Para o número de probabilidades de valores, leva o próximo valor mais alto após a mediana.
Giordano

6

Eu tenho este código abaixo que eu encontrei no HackerRank e é bastante simples e funciona em todos os 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 );

2
Eu acredito que isso só funciona com uma tabela que tem o número de entradas é ímpar. Para um número par de entradas, isso pode ter um problema.
Y. Chang

4

Com base na resposta do velcro, para aqueles que precisam fazer uma mediana de algo agrupado por outro parâmetro:

SELECIONE grp_field , t1 . val FROM ( SELECIONE grp_field , @ rownum : = SE (@ s = grp_field , @ rownum + 1 , 0 ) AS SELECT @ rownum : = 0 , @ s : = 0 ) r
   ORDER BY grp_field , d . val
 ) como t1 JOIN ( SELECT 
         row_number , @ s : = IF (@ s = grp_field , @ s , grp_field ) COMO seg , d . val
   DE dados d , (
            
   grp_field , conte (*) como total_rows
   FROM data d
   GROUP BY grp_field
 ) como t2
 ON t1 . grp_field = t2 . grp_field
 ONDE t1 . número da linha   = piso ( total_rows / 2 ) +1 ;


3

Você pode usar a função definida pelo usuário encontrada aqui .


3
Parece o mais útil, mas não quero instalar software alfa instável que possa causar o travamento do mysql no meu servidor de produção :(
davr

6
Portanto, estude suas fontes para a função de interesse, corrija-as ou modifique-as conforme necessário e instale a "sua própria" versão estável e não-alfa, uma vez criada - como isso é pior do que ajustar sugestões de código menos comprovadas você está conseguindo SO?))
Alex Martelli

3

Tome cuidado com uma contagem de valores ímpares - fornece a média dos dois valores no meio nesse caso.

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

2

Meu código, eficiente sem tabelas ou variáveis ​​adicionais:

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;

3
Isso falhará em uma quantidade substancial de dados, porque GROUP_CONCATé limitado a 1023 caracteres, mesmo quando usado em outra função como essa.
Rob Van Dam

2

Opcionalmente, você também pode fazer isso em um procedimento armazenado:

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);

Obrigado por isso! O usuário deve estar ciente de que valores ausentes (NULL) são considerados valores. para evitar esse problema, adicione 'x NÃO É NULL em que condição.
Giordano

1
@giordano Em qual linha do código x IS NOT NULLdeve ser adicionada?
Przemyslaw Remin

1
@PrzemyslawRemin Desculpe, eu não estava claro na minha declaração e percebi agora que o SP já considera o caso de valores ausentes. O SP deve ser chamado desta forma: CALL median("table","x","x IS NOT NULL").
Giordano #

2

Minha solução apresentada abaixo funciona em apenas uma consulta sem criação de tabela, variável ou até subconsulta. Além disso, permite obter mediana para cada grupo em consultas agrupadas (é isso que eu precisava!):

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 devido ao uso inteligente de group_concat e substring_index.

Mas, para permitir grande group_concat, você deve definir group_concat_max_len para um valor mais alto (1024 caracteres por padrão). Você pode configurá-lo assim (para a sessão sql atual):

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

Mais informações sobre group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Outro riff na resposta de Velcrow, mas usa uma única tabela intermediária e aproveita a variável usada para numeração de linhas para obter a contagem, em vez de executar uma consulta extra para calculá-la. Também inicia a contagem, de modo que a primeira linha seja a linha 0, para permitir simplesmente usar Floor e Ceil para selecionar a (s) linha (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));

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

O exposto acima parece funcionar para mim.


Não está retornando a mediana correta para o número par de valores. Por exemplo, a mediana de {98,102,102,98}é 100mas seu código fornece 102. Funcionou bem para números ímpares.
Nomiluks

1

Eu usei uma abordagem de duas consultas:

  • primeiro a obter contagem, min, max e média
  • segundo (declaração preparada) com as cláusulas "LIMIT @ count / 2, 1" e "ORDER BY .." para obter o valor mediano

Eles são agrupados em uma função defn, para que todos os valores possam ser retornados de uma chamada.

Se seus intervalos forem estáticos e seus dados não forem alterados com frequência, pode ser mais eficiente pré-calcular / armazenar esses valores e usar os valores armazenados em vez de consultar do zero todas as vezes.


1

Como eu só precisava de uma solução mediana E percentil, criei uma função simples e bastante flexível com base nas descobertas neste tópico. Sei que me sinto feliz se encontrar funções "prontas" que sejam fáceis de incluir em meus projetos, por isso decidi compartilhar rapidamente:

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;
    }

}

O uso é muito fácil, exemplo do meu projeto atual:

...
$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);
...

1

Aqui está o meu caminho. Claro, você pode colocá-lo em um procedimento :-)

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;

Você poderia evitar a variável @median_counter, se a subestimar:

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;

1

Desta maneira, parece incluir tanto a contagem par quanto a ímpar sem subconsulta.

SELECT AVG(t1.x)
FROM table t1, table t2
GROUP BY t1.x
HAVING SUM(SIGN(t1.x - t2.x)) = 0

Você poderia dizer qual é a tabela t2?
xliiv

1

Com base na resposta de @ bob, isso generaliza a consulta para poder retornar várias medianas, agrupadas por alguns critérios.

Pense, por exemplo, no preço médio de venda de carros usados ​​em um lote de carros, agrupado por ano-mês.

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;

1

Frequentemente, talvez seja necessário calcular a mediana não apenas para toda a tabela, mas também para agregados com relação ao nosso ID. Em outras palavras, calcule a mediana de cada ID em nossa tabela, onde cada ID possui muitos registros. (bom desempenho e funciona em muitos SQL + corrige o problema de pares e probabilidades, mais sobre o desempenho 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 que ajude


É a melhor solução. No entanto, para conjuntos de dados grandes, ele fica mais lento porque conta novamente para cada item em cada conjunto. Para acelerar, coloque "COUNT (*)" para separar a subconsulta.
Slava Murygin 28/02

1

O MySQL suporta funções de janela desde a versão 8.0, você pode usar ROW_NUMBERou DENSE_RANK( NÃO use RANK, pois atribui a mesma classificação aos mesmos valores, como no ranking de esportes):

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));

0

Se o MySQL tiver ROW_NUMBER, o MEDIAN é (inspire-se nesta consulta do 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)
;

A entrada é usada caso você tenha um número par de entradas.

Se você deseja encontrar a mediana por grupo, basta PARTITION BY group em suas cláusulas OVER.

Roubar


1
Não, não ROW_NUMBER OVER, não há PARTIÇÃO POR, nada disso; este é o MySql, não um mecanismo de banco de dados real como o PostgreSQL, IBM DB2, MS SQL Server e assim por diante ;-).
28780 Alex Martelli

0

Depois de ler todos os anteriores, eles não correspondiam ao meu requisito real, então eu implementei o meu próprio que não precisa de nenhum procedimento ou instrução complicada, apenas GROUP_CONCATtodos os valores da coluna que eu queria obter a MEDIAN e aplicando uma COUNT DIV BY 2 Extraio o valor do meio da lista, como a seguinte consulta:

(POS é o nome da coluna em que quero obter sua 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 isso possa ser útil para alguém da maneira que muitos outros comentários foram feitos para mim neste site.


0

Conhecendo a contagem exata de linhas, você pode usar esta consulta:

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

Onde <half> = ceiling(<size> / 2.0) - 1


0

Eu tenho um banco de dados contendo cerca de 1 bilhão de linhas necessárias para determinar a idade média no conjunto. Classificar um bilhão de linhas é difícil, mas se você agregar os valores distintos que podem ser encontrados (as idades variam de 0 a 100), poderá classificar ESTA lista e usar alguma mágica aritmética para encontrar o percentil desejado da seguinte maneira:

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
;

Essa consulta depende das funções da janela de suporte do db (incluindo ROWS UNBOUNDED PRECEDING), mas se você não tiver, é simples associar o aggData CTE a si próprio e agregar todos os totais anteriores na coluna 'acumulada', usada para determinar quais O valor contém o precentil especificado. A amostra acima calcula p10, p25, p50 (mediana), p75 e p90.

-Chris


0

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

Eu sugeriria outra maneira, sem junção , mas trabalhando com strings

Eu não o verifiquei com tabelas com dados grandes, mas as tabelas pequenas / médias funcionam muito bem.

O bom aqui, que ele funciona também por GROUPING para que você possa retornar a mediana para vários itens.

aqui está o código de teste para a tabela de teste:

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 

e o código para encontrar a mediana de 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

Resultado:

grp | the_median| all_vals_for_debug
bike| 22        | 22,26
book| 4         | 2,2,3,4,7,8,9
note| 11        | 11

Você não acha que a mediana de `{22,26}` deve ser 24?
Nomiluks

0

Em alguns casos, a mediana é calculada da seguinte forma:

A "mediana" é o valor "intermediário" na lista de números quando eles são ordenados por valor. Para conjuntos de contagens pares, a mediana é a média dos dois valores médios . Eu criei um código simples para isso:

$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;

A mediana $ retornada seria o resultado necessário :-)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.