【问题标题】:Calculate Percentile Value using MySQL使用 MySQL 计算百分比值
【发布时间】:2013-11-15 04:59:00
【问题描述】:

我有一个包含数千行的表,我想计算其中一个字段的第 90 个百分位,称为“round”。

例如,选择第 90 个百分位的 round 值。

我没有看到在 MySQL 中执行此操作的直接方法。

有人可以就我如何开始这种计算提供一些建议吗?

谢谢!

【问题讨论】:

    标签: mysql percentile


    【解决方案1】:

    首先,假设您有一个包含值列的表。您想获得第 95 个百分位值的行。换句话说,您正在寻找一个大于所有值 95% 的值。
    这是一个简单的答案:

    SELECT * FROM 
    (SELECT t.*,  @row_num :=@row_num + 1 AS row_num FROM YOUR_TABLE t, 
        (SELECT @row_num:=0) counter ORDER BY YOUR_VALUE_COLUMN) 
    temp WHERE temp.row_num = ROUND (.95* @row_num); 
    

    【讨论】:

    • 这是一个非常紧凑且吸引人的答案。但是,对于高百分位数来说,对整个集合进行排序可能不是最理想的。我们只需要(1-percentile)最大值来获得百分位数,当只需要一小部分值(部分排序)时,mysql 可以优化 ORDER。为此,您可以使用 ORDER LIMIT 解决方案。
    • 这是一个非常有用的解决方案,但我需要在计数之前对数据进行分组。例如,让我们假设 5 天有 20 行;我需要五个不同的计数器,而不是 1 到 20 的计数器:可能首先是第 1 天的 1 到 4,接下来是第 2 天的 1 到 6,依此类推,直到 20 条记录和 5 天。这有没有可能?提前致谢!
    【解决方案2】:

    比较解决方案:

    在我的服务器上获得 130 万行的 99% 所用的秒数:

    • 使用索引限制 x,y 并且没有位置:0.01 seconds
    • 限制 x,y 没有 where:0.7 seconds
    • 限制 x,y 其中:2.3 seconds
    • Full scan 没有在哪里:1.6 seconds
    • Full scan 其中:5.7 seconds

    使用LIMIT x,y()的大表最快解决方案

    1. 获取值的计数:SELECT COUNT(*) AS cnt FROM t
    2. 获取第n个值,其中n = (cnt - 1) * (1 - 0.95)SELECT k FROM t ORDER BY k DESC LIMIT n,1

    这个方案需要两次查询,因为mysql不支持在LIMIT子句中指定变量,存储过程除外(可以是optimized with stored procedure)。通常额外的查询开销非常低

    如果为 k 列添加索引并且不使用复杂的 where 子句(例如 0.01 秒用于 100 万行的表,因为不需要排序),则可以进一步优化此解决方案。

    PHP中的实现示例(不仅可以计算列的百分位数,还可以计算表达式的百分位数):

    function get_percentile($table, $where, $expr, $percentile) {
      if ($where) $subq = "WHERE $where";
      else $subq = "";
    
      $r = query("SELECT COUNT(*) AS cnt FROM $table $subq");
      $w = mysql_fetch_assoc($r);
      $num = abs(round(($w['cnt'] - 1) * (100 - $percentile) / 100.0));
    
      $q = "SELECT ($expr) AS prcres FROM $table $subq ORDER BY ($expr) DESC LIMIT $num,1";
      $r = query($q);
      if (!mysql_num_rows($r)) return null;
      $w = mysql_fetch_assoc($r);
      return $w['prcres'];
    }
    
    // Usage example
    $time = get_percentile(
      "state", // table
      "service='Time' AND cnt>0 AND total>0", // some filter
      "total/cnt", // expression to evaluate
      80); // percentile
    

    【讨论】:

      【解决方案3】:

      SQL 标准支持 PERCENTILE_DISCPERCENTILE_CONT 逆向分布函数来精确地完成这项工作。至少在 Oracle、PostgreSQL、SQL Server、Teradata 中都有实现。不幸的是不在 MySQL 中。 But you can emulate PERCENTILE_DISC in MySQL 8如下:

      SELECT DISTINCT first_value(my_column) OVER (
        ORDER BY CASE WHEN p <= 0.9 THEN p END DESC /* NULLS LAST */
      ) x,
      FROM (
        SELECT
          my_column,
          percent_rank() OVER (ORDER BY my_column) p,
        FROM my_table
      ) t;
      

      这会根据您的 my_column 排序计算每一行的 PERCENT_RANK,然后找到百分比排名小于或等于 0.9 个百分位数的最后一行。

      这仅适用于MySQL 8+, which has window function support

      【讨论】:

        【解决方案4】:

        http://www.artfulsoftware.com/infotree/queries.php#68

        SELECT  
          a.film_id , 
          ROUND( 100.0 * ( SELECT COUNT(*) FROM film AS b WHERE b.length <= a.length ) / total.cnt, 1 )  
          AS percentile 
        FROM film a  
        CROSS JOIN (  
          SELECT COUNT(*) AS cnt  
          FROM film  
        ) AS total 
        ORDER BY percentile DESC; 
        

        对于非常大的表,这可能会很慢

        【讨论】:

        • 不回答问题 - 它检索自上而下的电影长度百分位排名。相反,我要求计算一个值,即位于值的第 90 个百分位的“round”字段值。
        • 使用where percentile between 89 and 91
        • 当我运行它时,我得到了一个未知的列 b.length。你有这个演示的源表数据的链接吗?
        • b.length 和 a.length 是有百分位的列。所以 a.round 和 b.round ig 你正在寻找一个名为 round 的字段
        • 我非常喜欢这个解决方案,虽然正如你所说,它真的很慢
        【解决方案5】:

        我试图解决这个问题已经有一段时间了,然后我找到了以下答案。老实说辉煌。即使对于大表也非常快(我使用它的表包含大约 500 万条记录,需要几秒钟)。

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

        您可以想象,只需将 table_name 和 field_name 替换为您的表和列的名称。

        更多信息请查看Roland Bouman的原帖

        【讨论】:

          【解决方案6】:

          根据 Tony_Pets 的回答,但正如我在类似问题上所指出的那样:我必须稍微更改计算,例如第 90 个百分位数 - “90/100 * COUNT(*) + 0.5”而不是“90/100 *计数(*)+ 1”。有时它会跳过有序列表中的百分位点之后的两个值,而不是为百分位选择下一个更高的值。也许整数舍入在 mysql 中的工作方式。

          即:

          .. SUBSTRING_INDEX(SUBSTRING_INDEX( GROUP_CONCAT(fieldValue ORDER BY fieldValue SEPARATOR ','), ',', 90/100 * COUNT(*) + 0.5), ',', -1) as 90thPercentile .. ..

          【讨论】:

            【解决方案7】:

            在 MySQL 8 中,您可以使用 ntile 窗口函数:

            SELECT SomeTable.ID, SomeTable.Round
            FROM SomeTable
            JOIN (
                SELECT SomeTable, (NTILE(100) OVER w) AS Percentile
                FROM SomeTable
                    WINDOW w AS (ORDER BY Round)
            ) AS SomeTablePercentile ON SomeTable.ID = SomeTablePercentile.ID
            WHERE Percentile = 90
            LIMIT 1
            

            https://dev.mysql.com/doc/refman/8.0/en/window-function-descriptions.html#function_ntile

            【讨论】:

              【解决方案8】:

              适用于 MySQL 8 的替代解决方案:生成数据的直方图

              ANALYZE TABLE my_table UPDATE HISTOGRAM ON my_column WITH 100 BUCKETS;
              

              然后只需从 information_schema.column_statistics 中选择第 95 条记录:

              SELECT v,c FROM information_schema.column_statistics, JSON_TABLE(histogram->'$.buckets', 
                   '$[*]' COLUMNS(v VARCHAR(60) PATH '$[0]', c double PATH '$[1]')) hist 
                   WHERE column_name='my_column' LIMIT 95,1
              

              瞧!你仍然需要决定是取百分位数的下限还是上限,或者取一个平均值——但现在这是一项小任务。最重要的是 - 这非常快,一旦构建了直方图对象。

              此解决方案的功劳:lefred's blog

              【讨论】:

                【解决方案9】:

                百分位数的最常见定义是某个分数低于该数字的数字。您可能知道您在考试中获得了 67 分(满分 90 分)。但除非你知道你属于哪个百分位,否则这个数字没有真正的意义。如果您知道自己的分数在第 95 个百分位,这意味着您的得分高于 95% 的参加考试的人。

                此解决方案也适用于较旧的 MySQL 5.7。

                SELECT *, @row_num as numRows, 100 - (row_num * 100/(@row_num)) as percentile
                FROM (
                    select *, @row_num := @row_num + 1 AS row_num 
                    from (
                      SELECT t.subject, pt.score, p.name
                      FROM test t, person_test pt, person p, (
                        SELECT @row_num := 0
                      ) counter 
                      where t.id=pt.test_id
                      and p.id=pt.person_id
                      ORDER BY score desc
                    ) temp
                ) temp2
                -- optional: filter on a minimal percentile (uncomment below)
                -- having percentile >= 80
                

                【讨论】:

                  猜你喜欢
                  • 2013-03-22
                  • 2012-09-20
                  • 1970-01-01
                  • 2015-10-11
                  • 2020-06-25
                  • 2022-01-23
                  • 2016-09-26
                  • 2012-04-05
                  相关资源
                  最近更新 更多