Einfache Methode zur Berechnung des Medians mit MySQL


207

Was ist der einfachste (und hoffentlich nicht zu langsame) Weg, um den Median mit MySQL zu berechnen? Ich habe AVG(x)den Mittelwert ermittelt, aber es fällt mir schwer, einen einfachen Weg zur Berechnung des Medians zu finden. Im Moment kehre ich alle Zeilen zu PHP zurück, sortiere sie und wähle dann die mittlere Zeile aus, aber es muss sicherlich eine einfache Möglichkeit geben, dies in einer einzelnen MySQL-Abfrage zu tun.

Beispieldaten:

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

Das Sortieren nach valgibt 2 2 3 4 7 8 9, also sollte der Median sein 4, gegenüber SELECT AVG(val)dem == 5.


71
Bin ich der einzige, dem die Tatsache übel ist, dass MySQL keine Funktion zur Berechnung eines Medians hat? Lächerlich.
Monica Heddneck

3
MariaDB seit Version 10.3 hat eine, siehe mariadb.com/kb/en/library/median
Berturion

Antworten:


224

In 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 weist darauf hin, dass @rownum nach dem ersten Durchgang die Gesamtzahl der Zeilen enthält. Dies kann verwendet werden, um den Median zu bestimmen, sodass kein zweiter Durchgang oder Join erforderlich ist.

Außerdem AVG(dd.val)und dd.row_number IN(...)wird verwendet, um einen Median korrekt zu erzeugen, wenn eine gerade Anzahl von Datensätzen vorhanden ist. Argumentation:

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

Schließlich enthält MariaDB 10.3.3+ eine MEDIAN-Funktion


4
Gibt es eine Möglichkeit, Gruppenwerte anzuzeigen? wie: Ort / Median für diesen Ort ... wie Ort auswählen, Medianwert aus Tabelle ... auf irgendeine Weise? danke
saulob

2
@rowNum hat am Ende der Ausführung die 'Gesamtzahl'. Sie können dies also verwenden, wenn Sie vermeiden möchten, dass Sie erneut alle zählen müssen (was mein Fall war, weil meine Abfrage nicht so einfach war)
Ahmed-Anas,

Die Logik einer Aussage: (Etage ((total_rows + 1) / 2), Etage ((total_rows + 2) / 2)) berechnet die für den Median benötigten Zeilen ist fantastisch! Ich bin mir nicht sicher, wie du das gedacht hast, aber es ist brillant. Der Teil, dem ich nicht folge, ist der (SELECT @rownum: = 0) r - welchem ​​Zweck dient dies?
Shanemeister

Ändern Sie die erste WHERE 1in, WHERE d.val IS NOT NULLso dass NULLZeilen ausgeschlossen werden, um diese Methode mit der nativeAVG
chiliNUT

1
Mein Wert stammte aus einem Join mit zwei Tabellen, daher musste ich eine weitere Unterabfrage hinzufügen, um sicherzustellen, dass die Zeilenreihenfolge nach dem Join korrekt war! Die Struktur war irgendwieselect 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

Ich habe gerade eine andere Antwort online in den Kommentaren gefunden :

Für Mediane in fast jedem 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

Stellen Sie sicher, dass Ihre Spalten gut indiziert sind und der Index zum Filtern und Sortieren verwendet wird. Überprüfen Sie mit den EXPLAIN-Plänen.

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

Berechnen Sie die "mittlere" Zeilennummer. Vielleicht verwenden : median_row = floor(count / 2).

Dann wählen Sie es aus der Liste:

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

Dies sollte Ihnen eine Zeile mit nur dem gewünschten Wert zurückgeben.

Jacob


6
@rob kannst du bitte beim bearbeiten helfen? Oder sollte ich mich einfach vor der Klettlösung verneigen? (
Ich bin

1
Beachten Sie, dass ein "Cross Join" ausgeführt wird, der für große Tabellen sehr langsam ist.
Rick James

1
Diese Antwort gibt für eine gerade Anzahl von Zeilen nichts zurück .
Kuttumiah

Diese Antwort funktioniert für einige Datensätze überhaupt nicht, z. B. für den trivialen Datensatz mit den Werten 0,1, 0,1, 0,1, 2 - sie funktioniert, wenn alle Werte unterschiedlich sind, aber nur, wenn die Werte
Kem Mason

32

Ich habe festgestellt, dass die akzeptierte Lösung bei meiner MySQL-Installation nicht funktioniert hat und einen leeren Satz zurückgegeben hat. Diese Abfrage hat jedoch in allen Situationen funktioniert, in denen ich sie getestet habe:

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
Absolut korrekt, funktioniert perfekt und sehr schnell auf meinen indizierten Tabellen
Rob

2
Dies scheint die schnellste Lösung auf MySQL unter allen Antworten hier zu sein, 200 ms mit knapp einer Million Datensätzen in der Tabelle
Rob

3
@FrankConijn: Es wird zweimal aus einer Tabelle ausgewählt. Der Name der Tabelle lautet dataund wird mit zwei Namen verwendet, xund y.
Brian

3
Ich habe nur gesagt, ich habe mein MySQL mit genau dieser Abfrage in einer Tabelle mit 33.000 Zeilen blockiert ...
Xenonite

1
Diese Abfrage gibt eine falsche Antwort für eine gerade Anzahl von Zeilen zurück.
Kuttumiah

26

Leider liefern weder die Antworten von TheJacobTaylor noch von velcrow genaue Ergebnisse für aktuelle Versionen von MySQL.

Die Antwort von Velcro von oben ist nah, wird jedoch für Ergebnismengen mit einer geraden Anzahl von Zeilen nicht korrekt berechnet. Mediane sind entweder definiert als 1) die mittlere Zahl bei ungeraden Sätzen oder 2) der Durchschnitt der beiden mittleren Zahlen bei geraden Sätzen.

Hier ist die Lösung für Klettverschlüsse, die sowohl für ungerade als auch für gerade Zahlenmengen geeignet ist:

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;

Befolgen Sie dazu die folgenden 3 einfachen Schritte:

  1. Ersetzen Sie "median_table" (2 Vorkommen) im obigen Code durch den Namen Ihrer Tabelle
  2. Ersetzen Sie "median_column" (3 Vorkommen) durch den Spaltennamen, für den Sie einen Median suchen möchten
  3. Wenn Sie eine WHERE-Bedingung haben, ersetzen Sie "WHERE 1" (2 Vorkommen) durch Ihre where-Bedingung

Und was tun Sie für den Median der Zeichenfolgenwerte?
Rick James

12

Ich schlage einen schnelleren Weg vor.

Holen Sie sich die Zeilenanzahl:

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

Nehmen Sie dann den Mittelwert in einer sortierten Unterabfrage:

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

Ich habe dies mit einem 5x10e6-Datensatz von Zufallszahlen getestet und er wird den Median in weniger als 10 Sekunden finden.


3
Warum nicht: SELECT val FROM data ORDER BY val limit @middlevalue, 1
Bryan

1
Wie ziehen Sie die variable Ausgabe Ihres ersten Codeblocks in Ihren zweiten Codeblock?
Reise

3
Wie in, woher kommt @middlevalue?
Reise

@Bryan - Ich stimme dir zu, das macht für mich viel mehr Sinn. Haben Sie jemals einen Grund gefunden, dies nicht so zu tun?
Shane N

5
Dies funktioniert nicht, da eine Variable nicht in der Limit-Klausel verwendet werden kann.
Codepk

8

Ein Kommentar auf dieser Seite in der MySQL-Dokumentation enthält den folgenden Vorschlag:

-- (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, dies ist eindeutig das Beste für Situationen, in denen Sie den Median aus einer komplizierten Teilmenge (n) benötigen (ich musste separate Mediane einer großen Anzahl von
Datenuntermengen

Funktioniert gut für mich. 5.6.14 MySQL Community Server. Eine Tabelle mit 11 Millionen Datensätzen (ca. 20 GB auf der Festplatte) enthält zwei nicht primäre Indizes (model_id, price). In der Tabelle (nach der Filtration) haben wir 500.000 Datensätze, für die der Median berechnet werden muss. Im Ergebnis haben wir 30.000 Datensätze (model_id, median_price). Die Abfragedauer beträgt 1,5-2 Sekunden. Geschwindigkeit ist schnell für mich.
Mikl

7

Installieren und verwenden Sie diese statistischen MySQL-Funktionen: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

Danach ist es einfach, den Median zu berechnen:

SELECT median(val) FROM data;

1
Ich habe es gerade selbst versucht und für das, was es wert ist, war die Installation super schnell / einfach und es funktionierte wie angekündigt, einschließlich Gruppierung, z. B. "Name auswählen, Median (x) FROM t1 Gruppe nach Name" - Github-Quelle hier: github.com/infusion/udf_infusion
Kem Mason

6

Die meisten der oben genannten Lösungen funktionieren nur für ein Feld der Tabelle. Möglicherweise müssen Sie den Median (50. Perzentil) für viele Felder in der Abfrage ermitteln.

Ich benutze das:

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;

Sie können die "50" im obigen Beispiel durch ein beliebiges Perzentil ersetzen, was sehr effizient ist.

Stellen Sie einfach sicher, dass Sie genügend Speicher für die GROUP_CONCAT haben. Sie können dies ändern mit:

SET group_concat_max_len = 10485760; #10MB max length

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


Beachten Sie: Für eine gerade Anzahl von Werten wird der höhere der beiden Mittelwerte verwendet. Für die Anzahl der Quotenwerte wird der nächsthöhere Wert nach dem Median verwendet.
Giordano

6

Ich habe diesen folgenden Code, den ich auf HackerRank gefunden habe, und er ist ziemlich einfach und funktioniert in jedem Fall.

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
Ich glaube, dass dies nur mit einer Tabelle funktioniert, bei der die Anzahl der Einträge ungerade ist. Bei einer geraden Anzahl von Einträgen kann dies ein Problem darstellen.
Y. Chang

4

Aufbauend auf der Antwort des Klettverschlusses für diejenigen unter Ihnen, die einen Median aus etwas machen müssen, das nach einem anderen Parameter gruppiert ist:

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 , ( 
         row_number
       SELECT @ rownum : = 0 , @ s : = 0 ) r
   ORDER BY grp_field , d . val
 ) als t1 JOIN ( SELECT grp_field , count (*) als total_rows
   FROM data d
   GROUP BY grp_field
 ) als t2
 ON t1 . grp_field = t2 . grp_field
 WO t1 . Zeilennummer     
     = Etage ( total_rows / 2 ) +1 ;


3

Sie könnten die benutzerdefinierte Funktion verwenden , die gefunden werden hier .


3
Dies sieht am nützlichsten aus, aber ich möchte keine instabile Alpha-Software installieren, die dazu führen kann, dass MySQL auf meinem Produktionsserver abstürzt :(
Davr

6
Untersuchen Sie also ihre Quellen auf die Funktion von Interesse, beheben Sie sie oder ändern Sie sie nach Bedarf, und installieren Sie "Ihre eigene" stabile und Nicht-Alpha-Version, sobald Sie sie erstellt haben - wie ist das schlimmer, als weniger bewährte Codevorschläge auf ähnliche Weise zu optimieren? Sie bekommen auf SO? -)
Alex Martelli

3

Kümmert sich um eine ungerade Wertzählung - gibt in diesem Fall den Durchschnitt der beiden Werte in der Mitte an.

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

Mein Code, effizient ohne Tabellen oder zusätzliche Variablen:

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
Dies schlägt bei einer erheblichen Datenmenge fehl, da GROUP_CONCATes auf 1023 Zeichen beschränkt ist, selbst wenn es in einer anderen Funktion wie dieser verwendet wird.
Rob Van Dam

2

Optional können Sie dies auch in einer gespeicherten Prozedur tun:

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

Danke dafür! Der Benutzer sollte sich bewusst sein, dass fehlende Werte (NULL) als Werte betrachtet werden. Um dieses Problem zu vermeiden, fügen Sie 'x IS NOT NULL where-Bedingung hinzu.
Giordano

1
@giordano In welcher Codezeile x IS NOT NULLsoll hinzugefügt werden?
Przemyslaw Remin

1
@PrzemyslawRemin Entschuldigung, ich war in meiner Aussage nicht klar und habe jetzt festgestellt, dass der SP den Fall fehlender Werte bereits berücksichtigt. Der SP sollte folgendermaßen aufgerufen werden : CALL median("table","x","x IS NOT NULL").
Giordano

2

Meine unten vorgestellte Lösung funktioniert in nur einer Abfrage, ohne dass eine Tabelle, eine Variable oder sogar eine Unterabfrage erstellt wird. Außerdem können Sie den Median für jede Gruppe in gruppenweisen Abfragen abrufen (dies ist das, was ich brauchte!):

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

Es funktioniert aufgrund einer intelligenten Verwendung von group_concat und substring_index.

Um jedoch big group_concat zuzulassen, müssen Sie group_concat_max_len auf einen höheren Wert setzen (standardmäßig 1024 Zeichen). Sie können es so einstellen (für die aktuelle SQL-Sitzung):

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

Weitere Informationen zu group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Ein weiteres Riff zu Velcrows Antwort, verwendet jedoch eine einzelne Zwischentabelle und nutzt die für die Zeilennummerierung verwendete Variable, um die Anzahl zu ermitteln, anstatt eine zusätzliche Abfrage zur Berechnung durchzuführen. Startet auch die Zählung, sodass die erste Zeile Zeile 0 ist, damit Sie mit Floor and Ceil einfach die mittleren Zeilen auswählen können.

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

Das obige scheint für mich zu funktionieren.


Es wird nicht der richtige Median für eine gerade Anzahl von Werten zurückgegeben. Der Median von {98,102,102,98}ist beispielsweise, 100aber Ihr Code gibt an 102. Bei ungeraden Zahlen hat es gut funktioniert.
Nomiluks

1

Ich habe einen Zwei-Abfrage-Ansatz verwendet:

  • Erster, der zählt, min, max und avg
  • zweite (vorbereitete Anweisung) mit den Klauseln "LIMIT @ count / 2, 1" und "ORDER BY ..", um den Medianwert zu erhalten

Diese sind in eine Funktion defn eingeschlossen, sodass alle Werte von einem Aufruf zurückgegeben werden können.

Wenn Ihre Bereiche statisch sind und sich Ihre Daten nicht häufig ändern, ist es möglicherweise effizienter, diese Werte vorab zu berechnen / zu speichern und die gespeicherten Werte zu verwenden, anstatt jedes Mal von Grund auf neu abzufragen.


1

Da ich nur eine Median- UND Perzentillösung benötigte, habe ich basierend auf den Ergebnissen in diesem Thread eine einfache und recht flexible Funktion erstellt. Ich weiß, dass ich selbst glücklich bin, wenn ich "Readymade" -Funktionen finde, die sich leicht in meine Projekte integrieren lassen. Deshalb habe ich beschlossen, schnell Folgendes zu teilen:

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

}

Die Verwendung ist sehr einfach, Beispiel aus meinem aktuellen Projekt:

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

Hier ist mein Weg. Natürlich könnte man es in eine Prozedur setzen :-)

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;

Sie könnten die Variable vermeiden @median_counter, wenn Sie sie unterteilen:

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

Dieser Weg scheint sowohl gerade als auch ungerade Zählungen ohne Unterabfrage einzuschließen.

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

1

Basierend auf der Antwort von @ bob wird die Abfrage so verallgemeinert, dass mehrere Mediane zurückgegeben werden können, die nach bestimmten Kriterien gruppiert sind.

Denken Sie beispielsweise an den mittleren Verkaufspreis für Gebrauchtwagen auf einem Autoparkplatz, gruppiert nach Jahr und Monat.

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

Oft müssen wir den Median nicht nur für die gesamte Tabelle berechnen, sondern auch für Aggregate in Bezug auf unsere ID. Mit anderen Worten, berechnen Sie den Median für jede ID in unserer Tabelle, wobei jede ID viele Datensätze enthält. (Gute Leistung und funktioniert in vielen SQL + behebt das Problem von Geraden und Gewinnchancen, mehr über die Leistung verschiedener Median-Methoden 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;

Ich hoffe es hilft


Es ist die beste Lösung. Bei großen Datenmengen wird es jedoch langsamer, da es für jedes Element in jedem Satz neu zählt. Um es schneller zu machen, setzen Sie "COUNT (*)" in eine separate Unterabfrage.
Slava Murygin

1

MySQL unterstützt seit Version 8.0 Fensterfunktionen, die Sie verwenden können ROW_NUMBERoder DENSE_RANK( NICHT verwenden, RANKda es denselben Werten denselben Rang zuweist wie im Sportranking):

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

Wenn MySQL ROW_NUMBER hat, lautet der MEDIAN (lassen Sie sich von dieser SQL Server-Abfrage inspirieren):

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

Das IN wird verwendet, wenn Sie eine gerade Anzahl von Einträgen haben.

Wenn Sie den Median pro Gruppe ermitteln möchten, klicken Sie in Ihren OVER-Klauseln einfach auf PARTITION BY-Gruppe.

rauben


1
Nein, nein ROW_NUMBER OVER, keine TEILUNG DURCH, nichts davon; Dies ist MySql, keine echte DB-Engine wie PostgreSQL, IBM DB2, MS SQL Server usw. ;-).
Alex Martelli

0

Nachdem ich alle vorherigen gelesen hatte, stimmten sie nicht mit meiner tatsächlichen Anforderung überein, also implementierte ich meine eigene, die keine Prozedur oder komplizierten Anweisungen benötigt, sondern nur GROUP_CONCATalle Werte aus der Spalte, in der ich den MEDIAN erhalten und einen COUNT DIV BY anwenden wollte 2 Ich extrahiere den Wert aus der Mitte der Liste wie bei der folgenden Abfrage:

(POS ist der Name der Spalte, deren Median ich erhalten möchte)

(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

Ich hoffe, dass dies für jemanden nützlich sein könnte, so wie viele andere Kommentare von dieser Website für mich waren.


0

Wenn Sie die genaue Zeilenanzahl kennen, können Sie diese Abfrage verwenden:

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

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


0

Ich habe eine Datenbank mit ungefähr 1 Milliarde Zeilen, die wir benötigen, um das Durchschnittsalter in der Menge zu bestimmen. Das Sortieren einer Milliarde Zeilen ist schwierig. Wenn Sie jedoch die verschiedenen Werte aggregieren, die gefunden werden können (Alter zwischen 0 und 100), können Sie DIESE Liste sortieren und mit arithmetischer Magie ein beliebiges Perzentil wie folgt finden:

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
;

Diese Abfrage hängt von Ihren db-unterstützenden Fensterfunktionen ab (einschließlich ROWS UNBOUNDED PRECEDING). Wenn Sie dies jedoch nicht tun, ist es einfach, aggData CTE mit sich selbst zu verbinden und alle vorherigen Summen in der Spalte 'akkumuliert' zusammenzufassen, anhand derer ermittelt wird, welche Wert enthält das angegebene Präzentil. Die obige Stichprobe berechnet p10, p25, p50 (Median), p75 und p90.

-Chris


0

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

Ich würde einen anderen Weg vorschlagen, ohne zu verbinden , aber mit Strings zu arbeiten

Ich habe es nicht mit Tabellen mit großen Daten überprüft, aber kleine / mittlere Tabellen funktioniert einwandfrei.

Das Gute dabei ist, dass es auch durch GROUPING funktioniert, sodass der Median für mehrere Elemente zurückgegeben werden kann.

Hier ist der Testcode für die Testtabelle:

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 

und den Code zum Finden des Medians für jede Gruppe:

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

Ausgabe:

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

Denken Sie nicht, dass der Median von {22,26} 24 sein sollte?
Nomiluks

0

In einigen Fällen wird der Median wie folgt berechnet:

Der "Median" ist der "mittlere" Wert in der Liste der Zahlen, wenn sie nach Wert geordnet sind. Bei geraden Zählsätzen ist der Median der Durchschnitt der beiden Mittelwerte . Ich habe dafür einen einfachen Code erstellt:

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

Der zurückgegebene $ -Median wäre das erforderliche Ergebnis :-)

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.