313

MySQL 中是否有一种复制 SQL Server 功能的好方法ROW_NUMBER()

例如:

SELECT 
    col1, col2, 
    ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY col3 DESC) AS intRow
FROM Table1

然后,例如,我可以添加一个限制为 1 的条件,以获得每对intRow最高的单行。col3(col1, col2)

4

25 回答 25

232

MySQL 中没有排名功能。您可以获得的最接近的是使用变量:

SELECT t.*, 
       @rownum := @rownum + 1 AS rank
  FROM YOUR_TABLE t, 
       (SELECT @rownum := 0) r

那么在我的情况下如何工作呢?我需要两个变量,col1 和 col2 各一个?当 col1 更改时,Col2 需要以某种方式重置..?

是的。如果是 Oracle,您可以使用 LEAD 函数在下一个值处达到峰值。值得庆幸的是,Quassnoi 涵盖了您需要在 MySQL 中实现的逻辑

于 2009-12-13T00:05:28.957 回答
117

我想要每个 (col1, col2) 对具有最高 col3 的行。

这是一个groupwise maximum,是最常见的 SQL 问题之一(因为它看起来应该很容易,但实际上并非如此)。

我经常为 null-self-join 感到高兴:

SELECT t0.col3
FROM table AS t0
LEFT JOIN table AS t1 ON t0.col1=t1.col1 AND t0.col2=t1.col2 AND t1.col3>t0.col3
WHERE t1.col1 IS NULL;

“获取表中没有匹配 col1,col2 的其他行具有更高 col3 的行。” (如果不止一行具有相同的 col1、col2、col3,您会注意到这一点以及大多数其他分组最大解决方案将返回多行。如果这是一个问题,您可能需要一些后处理。)

于 2009-12-13T00:14:14.047 回答
97

我总是最终遵循这种模式。鉴于此表:

+------+------+
|    i |    j |
+------+------+
|    1 |   11 |
|    1 |   12 |
|    1 |   13 |
|    2 |   21 |
|    2 |   22 |
|    2 |   23 |
|    3 |   31 |
|    3 |   32 |
|    3 |   33 |
|    4 |   14 |
+------+------+

你可以得到这个结果:

+------+------+------------+
|    i |    j | row_number |
+------+------+------------+
|    1 |   11 |          1 |
|    1 |   12 |          2 |
|    1 |   13 |          3 |
|    2 |   21 |          1 |
|    2 |   22 |          2 |
|    2 |   23 |          3 |
|    3 |   31 |          1 |
|    3 |   32 |          2 |
|    3 |   33 |          3 |
|    4 |   14 |          1 |
+------+------+------------+

通过运行此查询,它不需要定义任何变量:

SELECT a.i, a.j, count(*) as row_number FROM test a
JOIN test b ON a.i = b.i AND a.j >= b.j
GROUP BY a.i, a.j

希望有帮助!

于 2012-03-11T04:09:35.390 回答
68
SELECT 
    @i:=@i+1 AS iterator, 
    t.*
FROM 
    tablename AS t,
    (SELECT @i:=0) AS foo
于 2010-08-12T17:26:13.063 回答
39

MySQL 8.0.0及以上您可以本机使用窗口函数。

1.4 MySQL 8.0 的新特性

窗口函数。

MySQL 现在支持窗口函数,对于查询中的每一行,使用与该行相关的行执行计算。其中包括 RANK()、LAG() 和 NTILE() 等函数。此外,一些现有的聚合函数现在可以用作窗口函数;例如,SUM() 和 AVG()。

ROW_NUMBER() over_clause

返回其分区中当前行的编号。行数范围从 1 到分区行数。

ORDER BY 影响行编号的顺序。如果没有 ORDER BY,行编号是不确定的。

演示:

CREATE TABLE Table1(
  id INT AUTO_INCREMENT PRIMARY KEY, col1 INT,col2 INT, col3 TEXT);

INSERT INTO Table1(col1, col2, col3)
VALUES (1,1,'a'),(1,1,'b'),(1,1,'c'),
       (2,1,'x'),(2,1,'y'),(2,2,'z');

SELECT 
    col1, col2,col3,
    ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY col3 DESC) AS intRow
FROM Table1;

DBFiddle 演示

于 2017-10-15T10:01:04.213 回答
28

查看这篇文章,它展示了如何在 MySQL 中使用分区来模拟 SQL ROW_NUMBER()。我在 WordPress 实现中遇到了同样的场景。我需要 ROW_NUMBER() 但它不存在。

http://www.explodybits.com/2011/11/mysql-row-number/

本文中的示例使用单个按字段分区。要按附加字段进行分区,您可以执行以下操作:

  SELECT  @row_num := IF(@prev_value=concat_ws('',t.col1,t.col2),@row_num+1,1) AS RowNumber
         ,t.col1 
         ,t.col2
         ,t.Col3
         ,t.col4
         ,@prev_value := concat_ws('',t.col1,t.col2)
    FROM table1 t,
         (SELECT @row_num := 1) x,
         (SELECT @prev_value := '') y
   ORDER BY t.col1,t.col2,t.col3,t.col4 

使用 concat_ws 处理空值。我使用 int、date 和 varchar 对 3 个字段进行了测试。希望这可以帮助。查看文章,因为它分解了此查询并对其进行了解释。

于 2011-11-18T03:15:51.590 回答
19

我也会投票支持 Mosty Mostacho 的解决方案,只需对他的查询代码稍作修改:

SELECT a.i, a.j, (
    SELECT count(*) from test b where a.j >= b.j AND a.i = b.i
) AS row_number FROM test a

这将给出相同的结果:

+------+------+------------+
|    i |    j | row_number |
+------+------+------------+
|    1 |   11 |          1 |
|    1 |   12 |          2 |
|    1 |   13 |          3 |
|    2 |   21 |          1 |
|    2 |   22 |          2 |
|    2 |   23 |          3 |
|    3 |   31 |          1 |
|    3 |   32 |          2 |
|    3 |   33 |          3 |
|    4 |   14 |          1 |
+------+------+------------+

对于表:

+------+------+
|    i |    j |
+------+------+
|    1 |   11 |
|    1 |   12 |
|    1 |   13 |
|    2 |   21 |
|    2 |   22 |
|    2 |   23 |
|    3 |   31 |
|    3 |   32 |
|    3 |   33 |
|    4 |   14 |
+------+------+

唯一的区别是查询不使用 JOIN 和 GROUP BY,而是依赖嵌套选择。

于 2015-08-19T20:48:49.537 回答
12

我会定义一个函数:

delimiter $$
DROP FUNCTION IF EXISTS `getFakeId`$$
CREATE FUNCTION `getFakeId`() RETURNS int(11)
    DETERMINISTIC
begin
return if(@fakeId, @fakeId:=@fakeId+1, @fakeId:=1);
end$$

那么我可以这样做:

select getFakeId() as id, t.* from table t, (select @fakeId:=0) as t2;

现在您没有子查询,您不能在视图中拥有它。

于 2011-01-04T16:53:07.670 回答
10

在mysql中查询row_number

set @row_number=0;
select (@row_number := @row_number +1) as num,id,name from sbs
于 2015-11-05T09:20:44.423 回答
9

在 MySQL中没有类似rownum,的功能,row_num()但方法如下:

select 
      @s:=@s+1 serial_no, 
      tbl.* 
from my_table tbl, (select @s:=0) as s;
于 2016-12-27T09:15:38.623 回答
5

重要提示:请考虑升级到 MySQL 8+ 并使用已定义并记录在案的 ROW_NUMBER() 函数,并抛弃与功能受限的旧版 MySQL 相关的旧黑客

现在这是其中一个技巧:

此处使用查询中变量的答案大多/全部似乎忽略了文档所说的事实(释义):

不要依赖 SELECT 列表中的项目按从上到下的顺序进行评估。不要在一个 SELECT 项目中分配变量并在另一个项目中使用它们

因此,他们有可能会产生错误的答案,因为他们通常会做

select
  (row number variable that uses partition variable),
  (assign partition variable)

如果这些是自下而上评估的,行号将停止工作(无分区)

所以我们需要使用有保证执行顺序的东西。在以下情况下输入案例:

SELECT
  t.*, 
  @r := CASE 
    WHEN col = @prevcol THEN @r + 1 
    WHEN (@prevcol := col) = null THEN null
    ELSE 1 END AS rn
FROM
  t, 
  (SELECT @r := 0, @prevcol := null) x
ORDER BY col

作为大纲 ld,prevcol 的分配顺序很重要 - 在我们为其分配当前行的值之前,必须将 prevcol 与当前行的值进行比较(否则它将是当前行的 col 值,而不是前一行的 col 值) .

这是如何组合在一起的:

  • 评估第一个 WHEN。如果这一行的 col 与前一行的 col 相同,则 @r 递增并从 CASE 返回。此返回 led 值存储在 @r 中。MySQL 的一个特性是赋值将赋值给@r 的新值返回到结果行中。

  • 对于结果集的第一行,@prevcol 为 null(在子查询中初始化为 null),因此该谓词为 false。每次 col 更改时,第一个谓词也会返回 false(当前行与前一行不同)。这会导致第二个 WHEN 被评估。

  • 第二个 WHEN 谓词始终为假,它的存在纯粹是为了给 @prevcol 分配一个新值。因为这一行的 col 与前一行的 col 不同(我们知道这一点是因为如果它相同,则将使用第一个 WHEN),我们必须分配新值以保留它以供下次测试。因为进行了赋值,然后赋值的结果与 null 进行比较,任何与 null 相等的东西都是假的,所以这个谓词总是假的。但至少评估它完成了保留这一行的 col 值的工作,因此可以根据下一行的 col 值评估它

  • 因为第二个 WHEN 是假的,这意味着在我们通过 (col) 分区的列发生变化的情况下,它是 ELSE 为@r 提供了一个新值,从 1 重新开始编号

我们会遇到这样的情况:

SELECT
  t.*, 
  ROW_NUMBER() OVER(PARTITION BY pcol1, pcol2, ... pcolX ORDER BY ocol1, ocol2, ... ocolX) rn
FROM
  t

具有一般形式:

SELECT
  t.*, 
  @r := CASE 
    WHEN col1 = @pcol1 AND col2 = @pcol2 AND ... AND colX = @pcolX THEN @r + 1 
    WHEN (@pcol1 := pcol1) = null OR (@pcol2 := col2) = null OR ... OR (@pcolX := colX) = null THEN null
    ELSE 1 
  END AS rn
FROM
  t, 
  (SELECT @r := 0, @pcol1 := null, @pcol2 := null, ..., @pcolX := null) x
ORDER BY pcol1, pcol2, ..., pcolX, ocol1, ocol2, ..., ocolX

脚注:

  • pcol 中的 p 表示“分区”,ocol 中的 o 表示“顺序”——在一般形式中,我从变量名中删除了“prev”以减少视觉混乱

  • 周围的括号(@pcolX := colX) = null很重要。没有它们,您将 null 分配给 @pcolX 并且事情停止工作

  • 这是一个折衷方案,结果集也必须按分区列排序,以便与前一列进行比较。因此,您不能根据一列对行号进行排序,但将结果集排序到另一列您可能可以使用子查询来解决此问题,但我相信文档还指出,除非使用 LIMIT,否则子查询排序可能会被忽略,这可能会影响表现

  • 除了测试该方法是否有效之外,我还没有深入研究它,但是如果存在第二个 WHEN 中的谓词将被优化掉的风险(与 null 相比的任何内容都是 null/false,那么为什么还要麻烦运行分配)并且不执行,它也停止了。根据我的经验,这似乎不会发生,但如果可以合理发生,我很乐意接受评论并提出解决方案

  • 在创建@pcolX 变量的子查询中,将创建@pcolX 的空值转换为列的实际类型可能是明智的,即:select @pcol1 := CAST(null as INT), @pcol2 := CAST(null as DATE)

于 2019-03-05T06:56:50.307 回答
4

我发现效果最好的解决方案是使用这样的子查询:

SELECT 
    col1, col2, 
    (
        SELECT COUNT(*) 
        FROM Table1
        WHERE col1 = t1.col1
        AND col2 = t1.col2
        AND col3 > t1.col3
    ) AS intRow
FROM Table1 t1

PARTITION BY 列仅与“=”进行比较并用 AND 分隔。ORDER BY 列将与“<”或“>”进行比较,并用 OR 分隔。

我发现这非常灵活,即使它有点贵。

于 2013-02-21T16:21:25.060 回答
4

无法模仿行号功能。你可能会得到你期望的结果,但你很可能会在某个阶段感到失望。这是mysql文档所说的:

对于其他语句,例如 SELECT,您可能会得到预期的结果,但这不能保证。在下面的语句中,你可能会认为 MySQL 会先计算 @a,然后再进行赋值:SELECT @a, @a:=@a+1, ...; 但是,涉及用户变量的表达式的求值顺序是未定义的。

问候,乔治。

于 2015-02-11T09:35:40.560 回答
4

MariaDB 10.2 正在实现“窗口函数”,包括 RANK()、ROW_NUMBER() 和其他一些东西:

https://mariadb.com/kb/en/mariadb/window-functions/

根据本月在 Percona Live 上的一次演讲,它们得到了相当好的优化。

语法与问题中的代码相同。

于 2016-04-27T04:51:15.813 回答
3

MySQL从8.0+版本开始支持 ROW_NUMBER()

如果您使用 MySQL 8.0 或更高版本,请查看 ROW_NUMBER() 函数。否则,您必须模拟 ROW_NUMBER() 函数。

row_number() 是一个排名函数,它返回行的序号,第一行从 1 开始。

对于旧版本,

SELECT t.*, 
       @rowid := @rowid + 1 AS ROWID
  FROM TABLE t, 
       (SELECT @rowid := 0) dummy;
于 2019-05-14T06:15:26.070 回答
2

这允许在 MySQL 中实现 ROW_NUMBER() AND PARTITION BY 提供的相同功能

SELECT  @row_num := IF(@prev_value=GENDER,@row_num+1,1) AS RowNumber
       FirstName, 
       Age,
       Gender,
       @prev_value := GENDER
  FROM Person,
      (SELECT @row_num := 1) x,
      (SELECT @prev_value := '') y
  ORDER BY Gender, Age DESC
于 2014-05-10T00:45:46.223 回答
2

我没有看到任何涵盖“PARTITION BY”部分的简单答案,所以这是我的:

SELECT
    *
FROM (
    select
        CASE WHEN @partitionBy_1 = l THEN @row_number:=@row_number+1 ELSE @row_number:=1 END AS i
        , @partitionBy_1:=l AS p
        , t.*
    from (
        select @row_number:=0,@partitionBy_1:=null
    ) as x
    cross join (
        select 1 as n, 'a' as l
        union all
        select 1 as n, 'b' as l    
        union all
        select 2 as n, 'b' as l    
        union all
        select 2 as n, 'a' as l
        union all
        select 3 as n, 'a' as l    
        union all    
        select 3 as n, 'b' as l    
    ) as t
    ORDER BY l, n
) AS X
where i > 1
  • ORDER BY 子句必须反映您的 ROW_NUMBER 需求。因此,已经有一个明确的限制:您不能同时拥有此表单的多个 ROW_NUMBER“仿真”。
  • “计算列”的顺序很重要。如果您让 mysql 以其他顺序计算这些列,它可能无法正常工作。
  • 在这个简单的例子中,我只放了一个,但你可以有几个“PARTITION BY”部分

        CASE WHEN @partitionBy_1 = part1 AND @partitionBy_2 = part2 [...] THEN @row_number:=@row_number+1 ELSE @row_number:=1 END AS i
        , @partitionBy_1:=part1 AS P1
        , @partitionBy_2:=part2 AS P2
        [...] 
    FROM (
        SELECT @row_number:=0,@partitionBy_1:=null,@partitionBy_2:=null[...]
    ) as x
    
于 2015-06-16T09:21:49.007 回答
2

这也可能是一个解决方案:

SET @row_number = 0;

SELECT 
    (@row_number:=@row_number + 1) AS num, firstName, lastName
FROM
    employees
于 2018-05-11T13:27:40.060 回答
2

我想你可以在这里使用 DENSE_RANK() 函数。例子:

select `score`, DENSE_RANK() OVER( ORDER BY score desc ) as `rank` from Scores;

https://www.mysqltutorial.org/mysql-window-functions/mysql-dense_rank-function/

于 2021-12-28T16:56:14.400 回答
1

有点晚了,但也可能对寻找答案的人有所帮助...

在 rows/row_number 示例之间 - 可以在任何 SQL 中使用的递归查询:

WITH data(row_num, some_val) AS 
(
 SELECT 1 row_num, 1 some_val FROM any_table --dual in Oracle
  UNION ALL
 SELECT row_num+1, some_val+row_num FROM data WHERE row_num < 20 -- any number
)
SELECT * FROM data
 WHERE row_num BETWEEN 5 AND 10
/

ROW_NUM    SOME_VAL
-------------------
5           11
6           16
7           22
8           29
9           37
10          46
于 2013-03-21T20:08:48.627 回答
1

也有点晚了,但今天我有同样的需求,所以我在 Google 上进行了搜索,最后在 Pinal Dave 的文章http://blog.sqlauthority.com/2014/03/09/mysql-reset-row中找到了一个简单的通用方法-number-for-each-group-partition-by-row-number/

我想专注于 Paul 的原始问题(这也是我的问题),所以我将我的解决方案总结为一个工作示例。

因为我们想要在两列上进行分区,所以我会在迭代期间创建一个 SET 变量来确定是否启动了一个新组。

SELECT col1, col2, col3 FROM (
  SELECT col1, col2, col3,
         @n := CASE WHEN @v = MAKE_SET(3, col1, col2)
                    THEN @n + 1 -- if we are in the same group
                    ELSE 1 -- next group starts so we reset the counter
                END AS row_number,
         @v := MAKE_SET(3, col1, col2) -- we store the current value for next iteration
    FROM Table1, (SELECT @n := 0, @v := NULL) r -- helper table for iteration with startup values
   ORDER BY col1, col2, col3 DESC -- because we want the row with maximum value
) x WHERE row_number = 1 -- and here we select exactly the wanted row from each group

3 表示在 MAKE_SET 的第一个参数处我想要 SET 中的两个值(3=1|2)。当然,如果我们没有两个或更多列来构建组,我们可以消除 MAKE_SET 操作。构造完全相同。这根据需要为我工作。非常感谢 Pinal Dave 的清晰演示。

于 2014-11-15T12:31:31.207 回答
1

GROUP BY如果您的查询有语句,则使用交叉连接和逗号的解决方案将不起作用。对于这种情况,您可以使用子选择:

SELECT (@row_number := @row_number + 1) AS rowNumber, res.*
FROM
(
  SELECT SUM(r.amount) 
  FROM Results r 
  WHERE username = 1 
  GROUP BY r.amount
) res
CROSS JOIN (SELECT @row_number := 0) AS dummy
于 2021-01-28T22:27:55.253 回答
0

这不是最强大的解决方案 - 但如果您只是想在一个只有几个不同值的字段上创建一个分区排名,那么在逻辑包含您需要的尽可能多的变量时使用某些情况可能并不笨拙。

过去,这样的事情对我有用:

SELECT t.*, 
   CASE WHEN <partition_field> = @rownum1 := @rownum1 + 1 
     WHEN <partition_field> = @rownum2 := @rownum2 + 1 
     ...
     END AS rank
FROM YOUR_TABLE t, 
   (SELECT @rownum1 := 0) r1, (SELECT @rownum2 := 0) r2
ORDER BY <rank_order_by_field>
;

希望这有意义/有帮助!

于 2019-11-21T21:13:21.450 回答
0

MySQL 从版本 8 开始,支持 ROW_NUMBER(),因此您可以像在 SQL Server 中一样使用以下查询

SELECT 
    col1, col2, 
    ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY col3 DESC) AS intRow
FROM Table1

我还在 Maria DB 10.4.21 中对其进行了测试。它也在那里工作。

于 2022-02-19T16:06:41.960 回答
-1

当我们有多个列时,这对我来说非常适合创建 RowNumber。在本例中为两列。

SELECT @row_num := IF(@prev_value= concat(`Fk_Business_Unit_Code`,`NetIQ_Job_Code`), @row_num+1, 1) AS RowNumber, 
    `Fk_Business_Unit_Code`,   
    `NetIQ_Job_Code`,  
    `Supervisor_Name`,  
    @prev_value := concat(`Fk_Business_Unit_Code`,`NetIQ_Job_Code`)  
FROM (SELECT DISTINCT `Fk_Business_Unit_Code`,`NetIQ_Job_Code`,`Supervisor_Name`         
      FROM Employee    
      ORDER BY `Fk_Business_Unit_Code`, `NetIQ_Job_Code`, `Supervisor_Name` DESC) z,  
(SELECT @row_num := 1) x,  
(SELECT @prev_value := '') y  
ORDER BY `Fk_Business_Unit_Code`, `NetIQ_Job_Code`,`Supervisor_Name` DESC
于 2017-06-27T18:00:39.330 回答