Cách đơn giản để tính toán trung vị với MySQL


207

Cách đơn giản nhất (và hy vọng không quá chậm) để tính toán trung vị với MySQL là gì? Tôi đã sử dụng AVG(x)để tìm giá trị trung bình, nhưng tôi gặp khó khăn khi tìm một cách đơn giản để tính trung bình. Hiện tại, tôi đang trả lại tất cả các hàng cho PHP, thực hiện sắp xếp và sau đó chọn hàng giữa, nhưng chắc chắn phải có một số cách đơn giản để thực hiện trong một truy vấn MySQL duy nhất.

Dữ liệu ví dụ:

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

Sắp xếp theo valcho 2 2 3 4 7 8 9, vì vậy trung vị nên 4, so với SELECT AVG(val)đó == 5.


71
Tôi có phải là người duy nhất buồn nôn vì thực tế là MySQL không có chức năng tính toán trung vị? Nực cười.
Monica Heddneck

3
MariaDB kể từ phiên bản 10.3 có một, hãy xem mariadb.com/kb/en/l Library / media
berturion

Câu trả lời:


224

Trong 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 chỉ ra rằng, sau lần vượt qua đầu tiên, @rownum sẽ chứa tổng số hàng. Điều này có thể được sử dụng để xác định trung vị, do đó không cần vượt qua hoặc tham gia lần thứ hai.

Ngoài ra AVG(dd.val)dd.row_number IN(...)được sử dụng để sản xuất chính xác trung vị khi có số lượng bản ghi chẵn. Lý do:

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

Cuối cùng, MariaDB 10.3.3+ chứa hàm MEDIAN


4
bất kỳ cách nào để làm cho nó hiển thị giá trị nhóm? như: địa điểm / trung vị cho địa điểm đó ... như chọn địa điểm, median_value từ bảng ... có cách nào không? cảm ơn
saulob

2
@rowNum sẽ có 'tổng số' khi kết thúc thực hiện. Vì vậy, bạn có thể sử dụng điều đó nếu bạn muốn tránh phải thực hiện 'đếm lại tất cả' (đó là trường hợp của tôi vì truy vấn của tôi không đơn giản)
Ahmed-Anas

Logic của việc có một câu lệnh: (floor ((Total_rows + 1) / 2), floor ((Total_rows + 2) / 2)) tính toán các hàng cần cho trung vị là tuyệt vời! Không chắc bạn nghĩ thế nào về điều đó, nhưng nó thật tuyệt vời. Phần tôi không theo dõi là (SELECT @rownum: = 0) r - mục đích này phục vụ cho mục đích gì?
Shanemeister

thay đổi cái đầu tiên WHERE 1để WHERE d.val IS NOT NULLnó loại trừ NULLcác hàng để giữ cho phương thức này được căn chỉnh với bản địaAVG
chiliNUT

1
Giá trị của tôi đến từ một tham gia hai bảng, vì vậy tôi phải thêm một truy vấn con khác để đảm bảo thứ tự hàng là chính xác sau khi tham gia! Cấu trúc được sắp xếp theoselect 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

Tôi chỉ tìm thấy một câu trả lời trực tuyến trong các ý kiến :

Đối với trung vị trong hầu hết mọi 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

Đảm bảo các cột của bạn được lập chỉ mục tốt và chỉ mục được sử dụng để lọc và sắp xếp. Xác minh với các kế hoạch giải thích.

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

Tính số hàng "trung vị". Có thể sử dụng : median_row = floor(count / 2).

Sau đó chọn nó ra khỏi danh sách:

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

Điều này sẽ trả về cho bạn một hàng chỉ với giá trị bạn muốn.

Gia-cốp


6
@rob bạn có thể giúp chỉnh sửa không? Hay tôi chỉ nên cúi đầu xuống dung dịch velcrow? (không thực sự chắc chắn làm thế nào để trì hoãn một giải pháp khác) Cảm ơn, Jacob
TheJacobTaylor

1
Lưu ý rằng nó thực hiện "tham gia chéo", rất chậm đối với các bảng lớn.
Rick James

1
Câu trả lời này không trả về gì cho số hàng chẵn .
kuttumiah

Câu trả lời này hoàn toàn không hoạt động đối với một số bộ dữ liệu, ví dụ: bộ dữ liệu tầm thường với các giá trị 0,1, 0,1, 0,1, 2 - nó sẽ hoạt động nếu tất cả các giá trị là khác biệt, nhưng chỉ hoạt động nếu các giá trị
Kem Mason

32

Tôi thấy giải pháp được chấp nhận không hoạt động trên cài đặt MySQL của tôi, trả về một tập hợp trống, nhưng truy vấn này hoạt động với tôi trong tất cả các tình huống mà tôi đã thử nghiệm trên:

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
hoàn toàn chính xác, hoạt động hoàn hảo và rất nhanh trên các bảng được lập chỉ mục của tôi
Rob

2
đây dường như là giải pháp nhanh nhất trên mysql trong số tất cả các câu trả lời ở đây, 200ms chỉ với một triệu bản ghi trong bảng
Rob

3
@FrankConijn: Nó chọn từ một bảng hai lần. Tên của bảng là datavà nó đang được sử dụng với hai tên xy.
Brian

3
chỉ cần nói rằng tôi đã trì hoãn mysqld của mình với truy vấn chính xác này trên một bảng có 33k hàng ...
Xenonite

1
Truy vấn này trả về câu trả lời sai cho số hàng chẵn .
kuttumiah

26

Thật không may, cả câu trả lời của TheJacobTaylor và velcrow đều không trả về kết quả chính xác cho các phiên bản hiện tại của MySQL.

Câu trả lời của Velcro từ phía trên rất gần, nhưng nó không tính toán chính xác cho các tập kết quả có số hàng chẵn. Trung bình được định nghĩa là 1) số giữa trên các số được đánh số lẻ hoặc 2) trung bình của hai số giữa trên các bộ số chẵn.

Vì vậy, đây là giải pháp của velcro được vá để xử lý cả bộ số lẻ và số chẵn:

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;

Để sử dụng, hãy làm theo 3 bước đơn giản sau:

  1. Thay thế "median_table" (2 lần xuất hiện) trong mã trên bằng tên của bảng của bạn
  2. Thay thế "median_column" (3 lần xuất hiện) bằng tên cột bạn muốn tìm một trung vị cho
  3. Nếu bạn có điều kiện WHERE, hãy thay thế "WHERE 1" (2 lần xuất hiện) bằng điều kiện where của bạn

Và, bạn làm gì cho giá trị trung bình của chuỗi?
Rick James

12

Tôi đề xuất một cách nhanh hơn.

Lấy số hàng:

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

Sau đó lấy giá trị trung bình trong truy vấn con được sắp xếp:

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

Tôi đã thử nghiệm điều này với bộ dữ liệu 5x10e6 các số ngẫu nhiên và nó sẽ tìm thấy trung vị trong vòng dưới 10 giây.


3
Tại sao không: CHỌN val TỪ dữ liệu ĐẶT HÀNG theo giới hạn val @middlevalue, 1
Bryan

1
Làm thế nào để bạn kéo đầu ra biến của khối mã đầu tiên vào khối mã thứ hai của bạn?
Chuyến đi

3
Như trong, @middlevalue đến từ đâu?
Chuyến đi

@Bryan - Tôi đồng ý với bạn, điều đó có ý nghĩa hơn đối với tôi. Bạn đã bao giờ tìm thấy một lý do để không làm theo cách đó?
Shane N

5
Điều này không hoạt động như một biến không thể được sử dụng trong mệnh đề giới hạn.
codepk

8

Một nhận xét trên trang này trong tài liệu MySQL có gợi ý sau:

-- (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, cái này rõ ràng là tốt nhất cho các tình huống mà bạn cần trung vị từ một tập hợp con phức tạp (tôi cần tính toán các trung vị riêng biệt của một số lượng lớn các tập hợp dữ liệu)
mblackwell8

Hoạt động tốt cho tôi. 5.6.14 Máy chủ cộng đồng MySQL. Bảng có các bản ghi 11M (khoảng 20Gb trên đĩa), có hai chỉ mục không chính (model_id, price). Trong bảng (sau khi lọc) chúng ta có 500K hồ sơ để tính trung bình cho. Kết quả là chúng ta có 30K hồ sơ (model_id, median_price). Thời lượng truy vấn là 1,5-2 giây. Tốc độ là nhanh đối với tôi.
Mikl

7

Cài đặt và sử dụng các chức năng thống kê mysql này: http://www.xarg.org/2012/07/statistic-fifts-in-mysql/

Sau đó, tính toán trung vị là dễ dàng:

SELECT median(val) FROM data;

1
Tôi chỉ thử bản thân mình và với giá trị của nó, cài đặt nó cực nhanh / dễ dàng và nó hoạt động như quảng cáo, bao gồm cả nhóm, ví dụ: "chọn tên, trung vị (x) TỪ nhóm t1 theo tên" - nguồn github ở đây: github.com/infusion/udf_infusion
Kem Mason

6

Hầu hết các giải pháp trên chỉ hoạt động cho một trường của bảng, bạn có thể cần lấy trung vị (phân vị thứ 50) cho nhiều trường trên truy vấn.

Tôi sử dụng cái này:

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;

Bạn có thể thay thế "50" trong ví dụ trên bằng bất kỳ phân vị nào, rất hiệu quả.

Chỉ cần đảm bảo rằng bạn có đủ bộ nhớ cho GROUP_CONCAT, bạn có thể thay đổi nó bằng:

SET group_concat_max_len = 10485760; #10MB max length

Thêm chi tiết: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/


Lưu ý: Đối với số lượng giá trị chẵn, giá trị này sẽ cao hơn trong hai giá trị trung bình. Đối với số lượng giá trị tỷ lệ cược, nó sẽ lấy giá trị cao hơn tiếp theo sau trung vị.
giordano

6

Tôi có mã dưới đây mà tôi tìm thấy trên HackerRank và nó khá đơn giản và hoạt động trong từng trường hợp.

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
Tôi tin rằng điều này chỉ hoạt động với một bảng có số lượng mục là số lẻ. Đối với số lượng mục chẵn, điều này có thể có một vấn đề.
Y. Chang

4

Dựa trên câu trả lời của velcro, đối với những người bạn phải thực hiện trung gian với một cái gì đó được nhóm bởi một tham số khác:

CHỌN grp_field , t1 . val TỪ ( CHỌN grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   TỪ dữ liệu d , ( 
         row_number
       CHỌN @ rownum : = 0 , @ s : = 0 ) r
   TRÌNH TỰ DO grp_field , d . val
 ) t1 THAM GIA ( CHỌN grp_field , tính (*) dưới dạng tổng_bảng
   TỪ dữ liệu d
   NHÓM THEO grp_field
 ) t2
 TRÊN t1 . grp_field = t2 . grp_field
 Ở ĐÂU t1 . số lượng hàng     
     = sàn ( Total_rows / 2 ) +1 ;


3

Bạn có thể sử dụng chức năng do người dùng định nghĩa được tìm thấy ở đây .


3
Điều này có vẻ hữu ích nhất, nhưng tôi không muốn cài đặt phần mềm alpha không ổn định có thể khiến mysql bị sập vào máy chủ sản xuất của tôi :(
davr

6
Vì vậy, hãy nghiên cứu các nguồn của họ cho chức năng quan tâm, sửa chúng hoặc sửa đổi chúng khi cần và cài đặt phiên bản ổn định và không phải là "của riêng bạn" sau khi bạn thực hiện nó - làm thế nào tệ hơn điều chỉnh các đề xuất mã ít được chứng minh tương tự bạn có vào SO không? -)
Alex Martelli

3

Quan tâm đến số lượng giá trị lẻ - đưa ra avg của hai giá trị ở giữa trong trường hợp đó.

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

Mã của tôi, hiệu quả mà không cần bảng hoặc biến bổ sung:

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
Điều này sẽ thất bại trên bất kỳ số lượng dữ liệu đáng kể nào vì GROUP_CONCATbị giới hạn ở 1023 ký tự, ngay cả khi được sử dụng bên trong một chức năng khác như thế này.
Rob Van Dam

2

Tùy chọn, bạn cũng có thể làm điều này trong một thủ tục được lưu trữ:

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

Cảm ơn vì điều đó! Người dùng cần lưu ý rằng các giá trị bị thiếu (NULL) được coi là giá trị. để tránh vấn đề này, hãy thêm 'x IS KHÔNG NULL vào điều kiện.
giordano

1
@giordano Nên thêm dòng nào vào mã x IS NOT NULL?
Przemyslaw Remin

1
@PrzemyslawRemin Xin lỗi, tôi không rõ ràng trong tuyên bố của mình và bây giờ tôi nhận ra rằng SP đã xem xét trường hợp thiếu giá trị. SP nên được gọi theo cách này : CALL median("table","x","x IS NOT NULL").
giordano

2

Giải pháp của tôi được trình bày dưới đây chỉ hoạt động trong một truy vấn mà không tạo bảng, biến hoặc thậm chí truy vấn phụ. Thêm vào đó, nó cho phép bạn lấy trung bình cho mỗi nhóm trong các truy vấn theo nhóm (đây là những gì tôi cần!):

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

Nó hoạt động vì sử dụng thông minh group_concat và subopes_index.

Nhưng, để cho phép nhóm_concat lớn, bạn phải đặt group_concat_max_len thành giá trị cao hơn (1024 char theo mặc định). Bạn có thể đặt nó như thế (đối với phiên sql hiện tại):

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

Thêm thông tin cho nhóm_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Một câu hỏi khác về câu trả lời của Velcrow, nhưng sử dụng một bảng trung gian duy nhất và tận dụng biến được sử dụng để đánh số hàng để lấy số đếm, thay vì thực hiện một truy vấn bổ sung để tính toán. Đồng thời bắt đầu đếm để hàng đầu tiên là hàng 0 để cho phép chỉ cần sử dụng Tầng và Trần để chọn (các) hàng trung bình.

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

Ở trên dường như làm việc cho tôi.


Nó không trả về giá trị trung bình chính xác cho số lượng giá trị chẵn, Ví dụ: trung vị {98,102,102,98}100nhưng mã của bạn cho 102. Nó hoạt động tốt cho số lẻ.
Nomiluks

1

Tôi đã sử dụng một cách tiếp cận hai truy vấn:

  • đầu tiên để có được số lượng, tối thiểu, tối đa và avg
  • điều thứ hai (tuyên bố đã chuẩn bị) với các mệnh đề "LIMIT @ Count / 2, 1" và "ORDER BY .." để nhận giá trị trung bình

Chúng được gói trong một hàm defn, vì vậy tất cả các giá trị có thể được trả về từ một cuộc gọi.

Nếu phạm vi của bạn là tĩnh và dữ liệu của bạn không thay đổi thường xuyên, có thể hiệu quả hơn để tính toán trước / lưu trữ các giá trị này và sử dụng các giá trị được lưu trữ thay vì truy vấn từ đầu mỗi lần.


1

vì tôi chỉ cần một giải pháp trung bình VÀ phần trăm, tôi đã thực hiện một chức năng đơn giản và khá linh hoạt dựa trên những phát hiện trong chủ đề này. Tôi biết rằng bản thân tôi hạnh phúc nếu tôi tìm thấy các chức năng "readymade" dễ đưa vào các dự án của mình, vì vậy tôi quyết định nhanh chóng chia sẻ:

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

}

Cách sử dụng rất dễ dàng, ví dụ từ dự án hiện tại của tôi:

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

Đây là cách của tôi. Tất nhiên, bạn có thể đưa nó vào một thủ tục :-)

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;

Bạn có thể tránh biến @median_counter, nếu bạn thay thế nó:

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

Cách này dường như bao gồm cả số chẵn và số lẻ mà không có truy vấn con.

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

1

Dựa trên câu trả lời của @ bob, điều này khái quát hóa truy vấn để có khả năng trả về nhiều trung vị, được nhóm theo một số tiêu chí.

Hãy suy nghĩ, ví dụ, giá bán trung bình cho những chiếc xe đã qua sử dụng trong một lô xe, được nhóm theo năm.

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

Thông thường, chúng tôi có thể cần tính toán Median không chỉ cho toàn bộ bảng mà còn cho các tổng hợp liên quan đến ID của chúng tôi. Nói cách khác, tính toán trung vị cho mỗi ID trong bảng của chúng tôi, trong đó mỗi ID có nhiều bản ghi. (hiệu suất tốt và làm việc tại nhiều SQL + sửa chữa vấn đề chẵn và tỷ lệ cược, thêm về hoạt động của trung bình-phương pháp khác nhau 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;

Hy vọng nó giúp


Đó là giải pháp tốt nhất. Tuy nhiên, đối với các tập dữ liệu lớn, nó sẽ chậm lại vì nó tính lại cho mọi mục trong mỗi bộ. Để làm cho nó nhanh hơn, đặt "COUNT (*)" để tách truy vấn phụ.
Slava Murygin

1

MySQL đã hỗ trợ các chức năng cửa sổ kể từ phiên bản 8.0, bạn có thể sử dụng ROW_NUMBERhoặc DENSE_RANK( KHÔNG sử dụng RANKvì nó gán cùng một thứ hạng cho cùng các giá trị, như trong xếp hạng thể thao):

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

Nếu MySQL có ROW_NUMBER, thì MEDIAN là (được lấy cảm hứng từ truy vấn SQL Server này):

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

IN được sử dụng trong trường hợp bạn có số lượng mục chẵn.

Nếu bạn muốn tìm trung vị cho mỗi nhóm, thì chỉ cần THAM GIA B BYNG nhóm trong các mệnh đề TRÊN của bạn.

Cướp


1
Không, không ROW_NUMBER OVER, không có PHẦN THAM GIA, không ai trong số đó; đây là MySql, không phải là một công cụ DB thực sự như PostgreSQL, IBM DB2, MS SQL Server, v.v.-).
Alex Martelli

0

Sau khi đọc tất cả những cái trước đó, chúng không phù hợp với yêu cầu thực tế của tôi, vì vậy tôi đã thực hiện một yêu cầu riêng của mình mà không cần bất kỳ thủ tục hoặc tuyên bố phức tạp nào, chỉ cần tôi GROUP_CONCATtất cả các giá trị từ cột tôi muốn lấy MEDIAN và áp dụng COUNT DIV BY 2 Tôi trích xuất giá trị ở giữa danh sách như truy vấn sau đây:

(POS là tên của cột tôi muốn lấy trung vị của nó)

(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

Tôi hy vọng điều này có thể hữu ích cho ai đó theo cách mà nhiều bình luận khác dành cho tôi từ trang web này.


0

Biết chính xác số hàng bạn có thể sử dụng truy vấn này:

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

Ở đâu <half> = ceiling(<size> / 2.0) - 1


0

Tôi có một cơ sở dữ liệu chứa khoảng 1 tỷ hàng mà chúng tôi yêu cầu để xác định tuổi trung vị trong tập hợp. Sắp xếp một tỷ hàng rất khó, nhưng nếu bạn tổng hợp các giá trị riêng biệt có thể tìm thấy (độ tuổi từ 0 đến 100), bạn có thể sắp xếp danh sách NÀY và sử dụng một số phép thuật số học để tìm bất kỳ tỷ lệ phần trăm nào bạn muốn như sau:

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
;

Truy vấn này phụ thuộc vào các hàm cửa sổ hỗ trợ db của bạn (bao gồm cả ROWS UNBOUNDED PRECEDING) nhưng nếu bạn không có thì việc tham gia aggData CTE với chính nó và tổng hợp tất cả các tổng trước đó vào cột 'tích lũy' được sử dụng để xác định giá trị chứa các tiền tố được chỉ định. Các mẫu trên vôi hóa p10, p25, p50 (trung vị), p75 và p90.

-Chris


0

Lấy từ: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

Tôi sẽ đề xuất một cách khác, không tham gia , nhưng làm việc với các chuỗi

tôi đã không kiểm tra nó với các bảng có dữ liệu lớn, nhưng các bảng nhỏ / vừa nó hoạt động tốt.

Điều tốt ở đây là nó cũng hoạt động bằng cách NHÓM để nó có thể trả về trung vị cho một số vật phẩm.

Đây là mã kiểm tra cho bảng thử nghiệm:

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 

và mã để tìm trung vị cho mỗi nhóm:

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

Đầu ra:

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

Bạn không nghĩ trung vị của `{22,26}` phải là 24?
Nomiluks

0

Trong một số trường hợp, trung vị được tính như sau:

"Trung vị" là giá trị "giữa" trong danh sách các số khi chúng được sắp xếp theo giá trị. Đối với các tập hợp số chẵn, trung vị là trung bình của hai giá trị trung bình . Tôi đã tạo một mã đơn giản cho điều đó:

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

Giá trị trung bình $ trả về sẽ là kết quả bắt buộc :-)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.