尝试使用限制和偏移量进行排序时,mysql 查询中的结果重复

And*_*rew 3 mysql

小提琴和查询在这里,所以更容易找到,继续阅读问题本身。

SQL Fiddle 如果你们想弄乱它

我无法使用小提琴重现该问题。

这是您可以在小提琴中使用的查询

SELECT n.*, ns.notification_id AS is_read FROM notifications n
LEFT OUTER JOIN notification_status ns
ON n.id = ns.notification_id
LEFT JOIN notification_user_role nur
ON n.id = nur.notification_id
WHERE
(
  n.esb_consultant_id = 19291
  OR
  n.esb_consultant_id = 'role'
)
AND nur.user_role_id = 'pl_sso_regional_vice_president'
AND n.creation_date <= NOW()
AND n.expiration_date >= NOW()
ORDER BY n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC
LIMIT 0, 10
Run Code Online (Sandbox Code Playgroud)

我也在这篇文章中把它放在了较低的位置,但在这里更容易吸引眼球。


我会尽量保持简短。

我正在开发一个通知系统。我有下面描述的 3 个表。

我正在尝试以LIMIT10 个、分页、每页 10 个(因此OFFSET为 10 个)来获取通知。我正在使用 ajax 加载接下来的 10 个。

它们按优先级排序(从 1 到 6,首先显示 1,最后显示 6)。

所有未读通知必须首先显示(优先级仍然适用),而已读通知必须最后显示(优先级仍然适用)。

通知是每个角色。一个用户可以有多个角色(因此需要另一个表)。

下面notification_status描述的表格用于跟踪读取了哪些通知。

notification_status没有读取表中没有的通知。这是非常重要的。这个决定不是我做的。我只得忍受它。


为了把它放在大图中,让我们举个例子:

假设我们有 14 个通知:

其中 5 个将是优先级 1,未读。

其中 4 个优先级 > 1,未读。

其中 3 个将是优先级 1,阅读。

其中 2 个将优先级 > 1,阅读。


预期的显示顺序如下。

5 unread priority 1

4 unread priority > 1

1 read priority 1

ajax 从这里开始,因为我们每页有 10 个

2 read priority 1

2 read priority > 1


表结构如下。

notifications

+-------------------+------------------+------+-----+---------+----------------+
| Field             | Type             | Null | Key | Default | Extra          |
+-------------------+------------------+------+-----+---------+----------------+
| id                | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| type_id           | int(10) unsigned | NO   |     | NULL    |                |
| sticky            | int(10) unsigned | NO   |     | NULL    |                |
| priority          | int(10) unsigned | NO   |     | NULL    |                |
| esb_consultant_id | varchar(40)      | NO   |     |         |                |
| message_id        | varchar(100)     | NO   |     |         |                |
| esb_params        | varchar(255)     | YES  |     |         |                |
| creation_date     | datetime         | YES  |     | NULL    |                |
| expiration_date   | datetime         | YES  |     | NULL    |                |
+-------------------+------------------+------+-----+---------+----------------+
Run Code Online (Sandbox Code Playgroud)

notification_user_role

+-----------------+------------------+------+-----+---------+-------+
| Field           | Type             | Null | Key | Default | Extra |
+-----------------+------------------+------+-----+---------+-------+
| user_role_id    | varchar(150)     | NO   |     |         |       |
| notification_id | int(10) unsigned | NO   | MUL | NULL    |       |
+-----------------+------------------+------+-----+---------+-------+
Run Code Online (Sandbox Code Playgroud)

notification_status

+-------------------+------------------+------+-----+---------+-------+
| Field             | Type             | Null | Key | Default | Extra |
+-------------------+------------------+------+-----+---------+-------+
| esb_consultant_id | varchar(20)      | NO   |     |         |       |
| notification_id   | int(10) unsigned | NO   | MUL | NULL    |       |
+-------------------+------------------+------+-----+---------+-------+
Run Code Online (Sandbox Code Playgroud)

我用来检索结果的查询:

SELECT n.*, ns.notification_id AS is_read FROM notifications n
LEFT OUTER JOIN notification_status ns
ON n.id = ns.notification_id
LEFT JOIN notification_user_role nur
ON n.id = nur.notification_id
WHERE 
(
  n.esb_consultant_id = :consultant_id 
  OR 
  n.esb_consultant_id = :role_all
)
AND nur.user_role_id = :consultant_role
AND n.creation_date <= NOW()
AND n.expiration_date >= NOW()
ORDER BY n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC
LIMIT $offset, $limit
Run Code Online (Sandbox Code Playgroud)

$offset 是页面乘以 10 - 所以如果页面是 0(第一页)偏移量是 0,如果页面是 1(第一次 ajax 调用)偏移量是 10 等等

$limit 是极限,它总是 10。

:consultant_id 是用户 ID - 唯一

:role_all是一个简单的字符串all。它用于当某些通知适用于所有角色时(例如生日通知)。无论角色如何,所有用户都会收到此通知,因为他们都有生日。


问题:

每当我进行 ajax 调用时,我都会收到某些重复的通知。我只会发布它的屏幕截图,因为它比绘制它更容易。

请注意,ajax 本身只是我检索结果的方式的一部分,但不对它们自身的重复负责,我绝对确定。这也不是显示问题,我已经检查了两次和三次。

在阿贾克斯之前

阿贾克斯之后

我注意到的是,如果我要删除这部分

ORDER BY n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC

从查询。它工作正常。没有重复。

上面查询的转储,删除了限制并删除了顺序:

倾倒

对不起,图片更容易。


我正在使用 PHP 来查询数据库。

  public function all($consultant_id, $consultant_role, $offset = 0) {
    $limit = 10;
    $offset = $offset * 10;

    $query = <<<SQL
SELECT n.*, ns.notification_id AS is_read FROM notifications n
LEFT OUTER JOIN notification_status ns
ON n.id = ns.notification_id
LEFT JOIN notification_user_role nur
ON n.id = nur.notification_id
WHERE 
(
  n.esb_consultant_id = :consultant_id 
  OR 
  n.esb_consultant_id = :role_all
)
AND nur.user_role_id = :consultant_role
AND n.creation_date <= NOW()
AND n.expiration_date >= NOW()
ORDER BY n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC
LIMIT $offset, $limit
SQL;
    $return = $this->connection
      ->query($query
        , [
          ':consultant_role' => $consultant_role,
          ':consultant_id'   => $consultant_id,
          ':role_all'        => NotificationStatus::PL_N_ALL,
        ]
      )->fetchAll(\PDO::FETCH_ASSOC);

    foreach($return as $item) { // this is added simply for display purposes
      echo $item['id'] . '<br>';
    }

    return $return;
  }
Run Code Online (Sandbox Code Playgroud)

以上是用于检索结果的代码的复制+粘贴。该函数只返回显示结果,没有其他魔法。

foreach被添加到简单地显示在浏览器中的结果。

是输出的图像。通知 10 重复。

是完全相同的代码,只是 ORDER BY n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC删除了。限制和偏移在这里仍然适用。


我一般不太擅长 mysql 或 sql,我不确定问题本身出在哪里。

任何指向正确方向的人都非常感谢。即使是变通方法或“黑客”我也能接受。


mysql> SELECT * FROM notification_status WHERE notification_id = 10;
Empty set (0.00 sec)

mysql> SELECT * FROM notification_user_role WHERE notification_id = 10;
+--------------------------------+-----------------+
| user_role_id                   | notification_id |
+--------------------------------+-----------------+
| pl_sso_regional_vice_president |              10 |
+--------------------------------+-----------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM notifications WHERE id = 10;
+----+---------+--------+----------+-------------------+-------------------------------------+------------+---------------------+---------------------+
| id | type_id | sticky | priority | esb_consultant_id | message_id                          | esb_params | creation_date       | expiration_date     |
+----+---------+--------+----------+-------------------+-------------------------------------+------------+---------------------+---------------------+
| 10 |       2 |      0 |        4 | role              | pl_n_325f1676e8a263c86432edc7b9f09c | NULL       | 2018-02-01 00:00:00 | 2018-02-28 00:00:00 |
+----+---------+--------+----------+-------------------+-------------------------------------+------------+---------------------+---------------------+
1 row in set (0.00 sec)

mysql>
Run Code Online (Sandbox Code Playgroud)

ype*_*eᵀᴹ 12

您是否有与 2 个或更多角色相关联的通知?这将解释重复的结果。

从模式中我们可以推断出一个通知可以与许多雕像相关联,也可以与许多角色相关联。因此,如果一个通知与许多(比如 3 个)角色相关联,那么它的每个状态都会出现很多 (3) 次。与 1 个角色关联的通知不显示任何重复。

我们还注意到该SELECT列表不包括角色表中的任何列。这有助于解决方案:将JOINto table 角色转换为EXISTS子查询。查询变为:

SELECT 
    n.*, ns.notification_id AS is_read 
FROM 
    notifications n
    LEFT OUTER JOIN notification_status ns
    ON n.id = ns.notification_id
WHERE EXISTS
      ( SELECT 1
        FROM notification_user_role nur
        WHERE n.id = nur.notification_id 
          AND nur.user_role_id = :consultant_role
      )
  AND n.esb_consultant_id IN (:consultant_id, :role_all)
  AND n.creation_date <= NOW()
  AND n.expiration_date >= NOW()
ORDER BY 
    n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC
LIMIT 
    $offset, $limit 
Run Code Online (Sandbox Code Playgroud)

另一个原因可能是您有许多通知状态。在这种情况下,解决方案取决于您想对此做什么。(即通知具有 2 个或更多状态意味着什么,它是否应该在结果中出现一次或两次等)。
(但在这种情况下这不是问题)


您可能会得到“重复”的第三个原因是您没有明确的(完整的)ORDER BY子句。也就是说,当ORDER BY无法解决所有关系时。在这种情况下,当有关系时(比如在位置 8 到 28),LIMIT 10 OFFSET 0意思是“给我 10 行,按那个排序”。但是“那个”仅足以指定前 7 行。最后的第 8 个、第 9 个和第 10 个并列(还有 18 个!)因此 MySQL 决定并任意选择其中的 3 个。(仍然没有重复)

但是在下一个查询中,当您使用 请求第 11 到 20 行时OFFSET 10 LIMIT 10,MySQL 会以不同的方式解决关系,并且可能会再次为您提供您之前看到的 3 行(现在显示为第 11、14 和 15 行!)。

为什么这样做?因为它可以,而且你还没有告诉她一个明确而完整的方法来做ORDER BY.
顺便说一句,我不会称这些为“重复项”,因为它们出现在不同的结果集/调用中。稍有误导性的术语可能是无法轻易确定原因的原因。

然而,解决方案很简单。只需在子句中再添加一列(即uniqueORDER BY

ORDER BY 
    n.creation_date DESC, (is_read IS NULL) DESC, n.priority ASC,
    n.id    -- to resolve ties
Run Code Online (Sandbox Code Playgroud)

(或者,正如 OP 在评论和聊天室的讨论中澄清的那样,需要不同的顺序,最后仍然n.id是 ):

ORDER BY
    (is_read IS NULL) DESC, n.priority ASC, n.creation_date DESC,
    n.id DESC 
Run Code Online (Sandbox Code Playgroud)