Un moyen simple de calculer la médiane avec MySQL


208

Quelle est la façon la plus simple (et, espérons-le, pas trop lente) de calculer la médiane avec MySQL? J'ai utilisé AVG(x)pour trouver la moyenne, mais j'ai du mal à trouver un moyen simple de calculer la médiane. Pour l'instant, je renvoie toutes les lignes à PHP, en faisant un tri, puis en choisissant la ligne du milieu, mais il doit sûrement y avoir un moyen simple de le faire dans une seule requête MySQL.

Exemples de données:

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

Le tri sur valdonne 2 2 3 4 7 8 9, donc la médiane devrait être 4, par rapport à SELECT AVG(val)laquelle == 5.


72
suis-je le seul écœuré par le fait que MySQL n'a pas de fonction pour calculer une médiane? Ridicule.
Monica Heddneck

3
MariaDB depuis la version 10.3 en a un, voir mariadb.com/kb/en/library/median
berturion

Réponses:


225

Dans 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 fait remarquer qu'après la première passe, @rownum contiendra le nombre total de lignes. Cela peut être utilisé pour déterminer la médiane, donc aucune deuxième passe ou jointure n'est nécessaire.

Aussi AVG(dd.val)et dd.row_number IN(...)est utilisé pour produire correctement une médiane lorsqu'il existe un nombre pair d'enregistrements. Raisonnement:

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

Enfin, MariaDB 10.3.3+ contient une fonction MEDIAN


4
une façon de faire pour afficher les valeurs de groupe? comme: place / médiane pour cet endroit ... comme sélectionner un endroit, valeur_médiane de la table ... de toute façon? merci
saulob

2
@rowNum aura le «nombre total» à la fin de l'exécution. Vous pouvez donc l'utiliser si vous voulez éviter d'avoir à refaire un «compte tout» (ce qui était mon cas parce que ma requête n'était pas si simple)
Ahmed-Anas

La logique d'avoir une seule déclaration: (étage ((total_rows + 1) / 2), étage ((total_rows + 2) / 2)) calculer les lignes nécessaires pour la médiane est génial! Je ne sais pas comment vous avez pensé à cela, mais c'est génial. La partie que je ne respecte pas est la (SELECT @rownum: = 0) r - à quoi cela sert-il?
Shanemeister

changez le premier WHERE 1pour WHERE d.val IS NOT NULLqu'il exclue les NULLlignes pour garder cette méthode alignée avec le natifAVG
chiliNUT

1
Ma valeur provenait d'une jointure à deux tables, j'ai donc dû ajouter une autre sous-requête afin de m'assurer que l'ordre des lignes était correct après la jointure! La structure était en quelque sorteselect 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

Je viens de trouver une autre réponse en ligne dans les commentaires :

Pour les médianes dans presque tous les 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

Assurez-vous que vos colonnes sont bien indexées et que l'index est utilisé pour le filtrage et le tri. Vérifiez avec les plans d'explication.

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

Calculez le numéro de ligne «médian». Peut-être utiliser:median_row = floor(count / 2) .

Ensuite, choisissez-le dans la liste:

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

Cela devrait vous renvoyer une ligne avec juste la valeur souhaitée.

Jacob


6
@rob pouvez-vous aider à modifier s'il vous plaît? Ou devrais-je simplement me prosterner devant la solution velcrow? (pas vraiment sûr de savoir comment s'en remettre à une autre solution) Merci, Jacob
TheJacobTaylor

1
Notez qu'il effectue une "jointure croisée", ce qui est très lent pour les grandes tables.
Rick James

1
Cette réponse ne renvoie rien pour le nombre pair de lignes.
kuttumiah

Cette réponse ne fonctionne pas du tout pour certains ensembles de données, par exemple, l'ensemble de données trivial avec des valeurs 0,1, 0,1, 0,1, 2 - cela fonctionnera si toutes les valeurs sont distinctes, mais ne fonctionne que si les valeurs
Kem Mason

32

J'ai trouvé que la solution acceptée ne fonctionnait pas sur mon installation MySQL, renvoyant un ensemble vide, mais cette requête a fonctionné pour moi dans toutes les situations sur lesquelles je l'ai testée:

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
absolument correct, fonctionne parfaitement et très rapidement sur mes tables indexées
Rob

2
cela semble être la solution la plus rapide sur mysql parmi toutes les réponses ici, 200 ms avec juste un million d'enregistrements dans le tableau
Rob

3
@FrankConijn: Il sélectionne deux fois dans une table. Le nom de la table est dataet il est utilisé avec deux noms, xet y.
Brian

3
je dis juste que j'ai bloqué mon mysqld avec cette requête exacte sur une table avec 33k lignes ...
Xenonite

1
Cette requête renvoie une mauvaise réponse pour le nombre pair de lignes.
kuttumiah

26

Malheureusement, ni les réponses de TheJacobTaylor ni celles de velcrow ne renvoient des résultats précis pour les versions actuelles de MySQL.

La réponse du Velcro ci-dessus est proche, mais il ne calcule pas correctement pour les jeux de résultats avec un nombre pair de lignes. Les médianes sont définies comme 1) le nombre du milieu sur les ensembles de nombres impairs, ou 2) la moyenne des deux nombres du milieu sur les ensembles de nombres pairs.

Voici donc la solution de velcro corrigée pour gérer les ensembles de nombres pairs et impairs:

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;

Pour l'utiliser, suivez ces 3 étapes faciles:

  1. Remplacez "median_table" (2 occurrences) dans le code ci-dessus par le nom de votre table
  2. Remplacez "colonne_médiane" (3 occurrences) par le nom de la colonne pour laquelle vous souhaitez trouver une médiane
  3. Si vous avez une condition WHERE, remplacez "WHERE 1" (2 occurrences) par votre condition WHERE

Et que faites-vous pour la médiane des valeurs de chaîne?
Rick James

12

Je propose un moyen plus rapide.

Obtenez le nombre de lignes:

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

Prenez ensuite la valeur intermédiaire dans une sous-requête triée:

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

J'ai testé cela avec un ensemble de données 5x10e6 de nombres aléatoires et il trouvera la médiane en moins de 10 secondes.


3
Pourquoi pas: SELECT val FROM data ORDER BY val limit @middlevalue, 1
Bryan

1
Comment tirez-vous la sortie variable de votre premier bloc de code dans votre deuxième bloc de code?
Voyage

3
Comme dans, d'où vient @middlevalue?
Voyage

@Bryan - Je suis d'accord avec vous, cela a beaucoup plus de sens pour moi. Avez-vous déjà trouvé une raison de ne pas procéder ainsi?
Shane N

5
Cela ne fonctionne pas car une variable ne peut pas être utilisée dans la clause limite.
codepk

8

Un commentaire sur cette page dans la documentation MySQL a la suggestion suivante:

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

À mon humble avis, celui-ci est clairement le meilleur pour les situations où vous avez besoin de la médiane d'un sous-ensemble compliqué (j'avais besoin de calculer des médianes séparées d'un grand nombre de sous-ensembles de données)
mblackwell8

Fonctionne bien pour moi. 5.6.14 MySQL Community Server. La table avec 11 millions d'enregistrements (environ 20 Go sur le disque), possède deux index non principaux (model_id, price). Dans le tableau (après filtration), nous avons 500 enregistrements pour calculer la médiane. En conséquence, nous avons 30K enregistrements (model_id, median_price). La durée de la requête est de 1,5 à 2 secondes. La vitesse est rapide pour moi.
Mikl

8

Installez et utilisez ces fonctions statistiques mysql: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

Après cela, calculer la médiane est facile:

SELECT median(val) FROM data;

1
Je viens de l'essayer moi-même, et pour ce que ça vaut, l'installer était super rapide / facile, et cela a fonctionné comme annoncé, y compris le regroupement, par exemple "sélectionner le nom, médiane (x) FROM t1 group by name" - source github ici: github.com/infusion/udf_infusion
Kem Mason

6

La plupart des solutions ci-dessus ne fonctionnent que pour un champ de la table, vous devrez peut-être obtenir la médiane (50e centile) pour de nombreux champs de la requête.

J'utilise ceci:

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;

Vous pouvez remplacer le "50" dans l'exemple ci-dessus à n'importe quel centile, c'est très efficace.

Assurez-vous simplement que vous disposez de suffisamment de mémoire pour le GROUP_CONCAT, vous pouvez le changer avec:

SET group_concat_max_len = 10485760; #10MB max length

Plus de détails: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/


Attention: pour un nombre pair de valeurs, il faut la plus élevée des deux valeurs moyennes. Pour le nombre de cotes de valeurs, il faut la valeur immédiatement supérieure après la médiane.
giordano

6

J'ai ce code ci-dessous que j'ai trouvé sur HackerRank et il est assez simple et fonctionne dans tous les cas.

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
Je crois que cela ne fonctionne qu'avec une table dont le nombre d'entrées est impair. Pour un nombre pair d'entrées, cela peut avoir un problème.
Y. Chang

4

S'appuyant sur la réponse du velcro, pour ceux d'entre vous qui doivent faire une médiane de quelque chose qui est groupé par un autre paramètre:

SELECT grp_field , t1 . val FROM ( SELECT grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   FROM data d , SELECT 
         row_number
        (  @ rownum : = 0 , @ s : = 0 ) r
   ORDER BY grp_field , d . val
 ) as t1 JOIN ( SELECT grp_field , count (*) as total_rows
   FROM data d
   GROUP BY grp_field
 ) as t2
 ON t1 . grp_field = t2 . grp_field
 t1 . row_number    
     = étage ( total_rows / 2 ) +1 ;


3

Vous pouvez utiliser la fonction définie par l'utilisateur qui se trouve ici .


3
Cela semble le plus utile, mais je ne veux pas installer un logiciel alpha instable qui pourrait faire planter mysql sur mon serveur de production :(
davr

6
Alors étudiez leurs sources pour la fonction qui vous intéresse, corrigez-les ou modifiez-les selon vos besoins, et installez "votre propre" version stable et non alpha une fois que vous l'avez faite - comment est-ce pire que de peaufiner de la même manière des suggestions de code moins éprouvées vous obtenez sur SO? -)
Alex Martelli

3

Prend soin d'un nombre de valeurs impaires - donne la moyenne des deux valeurs au milieu dans ce cas.

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

Mon code, efficace sans tables ni variables supplémentaires:

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
Cela échouera sur toute quantité importante de données car GROUP_CONCATlimité à 1023 caractères, même lorsqu'il est utilisé dans une autre fonction comme celle-ci.
Rob Van Dam

2

Vous pouvez également le faire dans une procédure stockée:

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

Merci pour cela! L'utilisateur doit savoir que les valeurs manquantes (NULL) sont considérées comme des valeurs. pour éviter ce problème, ajoutez 'x IS NOT NULL where condition.
giordano

1
@giordano Dans quelle ligne du code x IS NOT NULLfaut-il ajouter?
Przemyslaw Remin,

1
@PrzemyslawRemin Désolé, je n'ai pas été clair dans ma déclaration et je me suis rendu compte maintenant que le SP considère déjà le cas des valeurs manquantes. Le SP devrait être appelé ainsi: CALL median("table","x","x IS NOT NULL").
giordano

2

Ma solution présentée ci-dessous fonctionne en une seule requête sans création de table, variable ou même sous-requête. De plus, il vous permet d'obtenir la médiane pour chaque groupe dans les requêtes groupées (c'est ce dont j'avais besoin!):

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

Cela fonctionne grâce à une utilisation intelligente de group_concat et substring_index.

Mais, pour autoriser un grand group_concat, vous devez définir group_concat_max_len sur une valeur plus élevée (1024 caractères par défaut). Vous pouvez le définir comme ça (pour la session sql actuelle):

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

Plus d'infos pour group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Un autre riff sur la réponse de Velcrow, mais utilise une seule table intermédiaire et tire parti de la variable utilisée pour la numérotation des lignes pour obtenir le nombre, plutôt que d'effectuer une requête supplémentaire pour le calculer. Commence également le décompte de sorte que la première ligne soit la ligne 0 pour permettre simplement d'utiliser Floor et Ceil pour sélectionner la ou les lignes médianes.

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

Ce qui précède semble fonctionner pour moi.


Il ne renvoie pas la médiane correcte pour un nombre pair de valeurs. Par exemple, la médiane de {98,102,102,98}est 100mais votre code donne 102. Cela a bien fonctionné pour les nombres impairs.
Nomiluks

1

J'ai utilisé une approche à deux requêtes:

  • premier à obtenir le nombre, min, max et moy
  • deuxième (instruction préparée) avec les clauses "LIMIT @ count / 2, 1" et "ORDER BY .." pour obtenir la valeur médiane

Ceux-ci sont enveloppés dans une fonction defn, de sorte que toutes les valeurs peuvent être renvoyées à partir d'un seul appel.

Si vos plages sont statiques et que vos données ne changent pas souvent, il peut être plus efficace de précalculer / stocker ces valeurs et d'utiliser les valeurs stockées au lieu d'interroger à partir de zéro à chaque fois.


1

comme j'avais juste besoin d'une solution médiane ET centile, j'ai créé une fonction simple et assez flexible basée sur les résultats de ce fil. Je sais que je suis content moi-même si je trouve des fonctions "readymade" faciles à intégrer dans mes projets, j'ai donc décidé de partager rapidement:

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

}

L'utilisation est très simple, exemple de mon projet actuel:

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

Voici ma voie. Bien sûr, vous pouvez le mettre dans une procédure :-)

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;

Vous pourriez éviter la variable @median_counter, si vous la sous-estimez:

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

Cette méthode semble inclure le nombre pair et impair sans sous-requête.

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

Pourriez-vous nous dire quelle est la table t2?
xliiv Il y a

1

Sur la base de la réponse de @ bob, cela généralise la requête pour avoir la possibilité de renvoyer plusieurs médianes, regroupées selon certains critères.

Pensez, par exemple, au prix de vente médian des voitures d'occasion dans un lot de voitures, regroupé par année-mois.

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

Souvent, nous pouvons avoir besoin de calculer la médiane non seulement pour l'ensemble du tableau, mais pour les agrégats par rapport à notre ID. En d'autres termes, calculez la médiane de chaque ID dans notre tableau, où chaque ID a de nombreux enregistrements. (bonnes performances et fonctionne dans de nombreux problèmes SQL + corrige le problème des paires et des cotes, plus sur les performances des différentes méthodes médianes 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;

J'espère que ça aide


C'est la meilleure solution. Cependant, pour les grands ensembles de données, il ralentira car il compte à nouveau pour chaque élément de chaque ensemble. Pour accélérer, mettez "COUNT (*)" pour séparer la sous-requête.
Slava Murygin

1

MySQL prend en charge les fonctions de fenêtre depuis la version 8.0, vous pouvez utiliser ROW_NUMBERou DENSE_RANK( NE PAS utiliser RANKcar il attribue le même rang aux mêmes valeurs, comme dans le classement sportif):

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

Si MySQL a ROW_NUMBER, alors le MEDIAN est (être inspiré par cette requête 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)
;

L'IN est utilisé dans le cas où vous avez un nombre pair d'entrées.

Si vous voulez trouver la médiane par groupe, alors juste PARTITION PAR groupe dans vos clauses OVER.

Rob


1
Non, non ROW_NUMBER OVER, pas de PARTITION PAR, rien de tout cela; c'est MySql, pas un vrai moteur de base de données comme PostgreSQL, IBM DB2, MS SQL Server, etc. ;-).
Alex Martelli

0

Après avoir lu tous les précédents, ils ne correspondaient pas à mes besoins réels, j'ai donc mis en œuvre le mien qui n'a besoin d'aucune procédure ou déclaration compliquée, juste je GROUP_CONCAT toutes les valeurs de la colonne que je voulais obtenir le MEDIAN et l' application d' un COUNT DIV PAR 2 J'extrais la valeur du milieu de la liste comme le fait la requête suivante:

(POS est le nom de la colonne dont je veux obtenir la médiane)

(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

J'espère que cela pourrait être utile pour quelqu'un dans la mesure où de nombreux autres commentaires étaient pour moi sur ce site.


0

Connaissant le nombre exact de lignes, vous pouvez utiliser cette requête:

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

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


0

J'ai une base de données contenant environ 1 milliard de lignes dont nous avons besoin pour déterminer l'âge médian de l'ensemble. Il est difficile de trier un milliard de lignes, mais si vous agrégez les valeurs distinctes qui peuvent être trouvées (les âges vont de 0 à 100), vous pouvez trier CETTE liste et utiliser une magie arithmétique pour trouver le centile souhaité comme suit:

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
;

Cette requête dépend de vos fonctions de prise en charge de la base de données db (y compris ROWS UNBOUNDED PRECEDING) mais si vous ne l'avez pas, il est simple de joindre aggData CTE avec lui-même et d'agréger tous les totaux antérieurs dans la colonne `` accumulée '' qui est utilisée pour déterminer laquelle La valeur contient le précentile spécifié. L'échantillon ci-dessus calcule p10, p25, p50 (médiane), p75 et p90.

-Chris


0

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

Je suggérerais une autre façon, sans rejoindre , mais en travaillant avec des chaînes

je ne l'ai pas vérifié avec des tables avec de grandes données, mais des tables petites / moyennes cela fonctionne très bien.

La bonne chose ici, que ça marche aussi en GROUPING afin qu'il puisse retourner la médiane de plusieurs éléments.

voici le code de test pour la table de test:

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 

et le code pour trouver la médiane de chaque groupe:

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

Production:

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

Ne pensez-vous pas que la médiane de «{22,26}» devrait être de 24?
Nomiluks

0

Dans certains cas, la médiane est calculée comme suit:

La "médiane" est la valeur "moyenne" dans la liste des nombres lorsqu'ils sont classés par valeur. Pour les ensembles de nombres pairs, la médiane est la moyenne des deux valeurs moyennes . J'ai créé un code simple pour cela:

$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 médiane $ retournée serait le résultat requis :-)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.