消息传递应用的复杂 SQL 查询

Complex SQL query for messaging app

我正在使用 PostgreSQL 9.3.4 在 Django 1.6.2 应用程序中构建消息传递功能。在用户的 "Messages" 主页上,我将显示用户与其他用户的对话列表。每个对话 "tile" 或块将显示该对话中其他用户的图片和姓名、该对话中最后一条消息的发送日期,以及最后一条消息中的前 25 个字符。如果最后一条消息是由正在查看这些对话的用户发送的,我还会显示一个小 "reply" 图标。我的查询已经到了可以识别查看者和所有其他用户之间的所有对话的地步,但是我无法从用户和消息 tables 中提取我需要的字段。

我的 table(显示在底部)是用户、消息和对话。虽然我已经实现了我的 table 模式,因此用户和对话之间存在多对多关系,但一开始我将创建我的界面,以便用户只能向一个用户发送消息其他用户而不是多个用户。

当我 运行 我查询下面显示的数据时,我试图取回的是用户 3、4、5 的对话和用户 ID 及其关联的用户名,即最后一条消息在那次对话中,是谁发送的,以及发送的日期。相反,我收到错误:

ERROR: syntax error at or near "WHERE"

谁能帮我解决这个问题?我对速度比对优雅更感兴趣。

测试用例

conversation_user 中的数据链接 table:

 id | conversation_id | user_id 
----+-----------------+---------
  1 |               1 |      32
  2 |               1 |       3   <- want this
  3 |               2 |      32
  4 |               2 |       4   <- want this
  6 |               3 |       3
  7 |               3 |       1
  8 |               4 |      32
  9 |               4 |       5   <- want this
 10 |               5 |       7
 11 |               5 |       9

我想要的行 return。每条消息都是该对话中的最后一条消息。

conversation_id | user_id | username  | from_user | message | send_date
----------------+---------+-----------+-----------+---------+----------
 1              | 3       | user3     | u3 or u32 | <msg3>  | <date>
 2              | 4       | user4     | u4 or u32 | <msg4>  | <date>
 4              | 5       | user5     | u5 or u32 | <msg5>  | <date>

查询无效:

SELECT cu.conversation_id,
       cu.user_id,
       au.username,
       m.from_user,
       m.message,
       m.send_date
FROM conversation_user cu
INNER JOIN auth_user au ON cu.user_id = au.id
INNER JOIN message m ON cu.conversation_id = m.conversation_id
ORDER BY m.send_date DESC LIMIT 1
WHERE conversation_id IN
    (SELECT conversation_id
     FROM conversation_user
     WHERE user_id = 32)
  AND user_id != 32;

Table 定义

# auth_user
--------------+--------------------------+------------------------------
 id           | integer                  | not null default nextval(...
 username     | character varying(30)    | not null
Referenced by:
    TABLE "conversation_user" CONSTRAINT "conversation_user_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED
    TABLE "message" CONSTRAINT "message_from_user_id_fkey" FOREIGN KEY (from_user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED

# conversation
------------+--------------------------+--------------------------------
 id         | integer                  | not null default nextval(...
 start_date | timestamp with time zone | not null
Referenced by:
    TABLE "conversation_user" CONSTRAINT "conversation_id_refs_id_4344ca71" FOREIGN KEY (conversation_id) REFERENCES conversation(id) DEFERRABLE INITIALLY DEFERRED
    TABLE "message" CONSTRAINT "message_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversation(id) DEFERRABLE INITIALLY DEFERRED

# conversation_user
-----------------+---------+--------------------------------------------
 id              | integer | not null default nextval(...
 conversation_id | integer | not null
 user_id         | integer | not null
Foreign-key constraints:
    "conversation_id_refs_id_4344ca71" FOREIGN KEY (conversation_id) REFERENCES conversation(id) DEFERRABLE INITIALLY DEFERRED
    "conversation_user_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED

# message
     Column      |           Type           |
-----------------+--------------------------+---------------------------
 id              | integer                  | not null default nextval(...
 conversation_id | integer                  | not null
 from_user_id    | integer                  | not null
 to_user_uid     | integer                  | not null
 message         | text                     | not null
 send_date       | timestamp with time zone | not null
Foreign-key constraints:
    "message_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversation(id) DEFERRABLE INITIALLY DEFERRED
    "message_from_user_id_fkey" FOREIGN KEY (from_user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED

修复语法

基本上,您只需将 WHERE 条件移动到适当的位置,例如 :

SELECT  ...
FROM conversation_user cu
INNER JOIN ...
<b>WHERE conversation_id IN
    (SELECT conversation_id
     FROM conversation_user
     WHERE user_id = 32)
AND user_id != 32</b>
ORDER BY m.send_date DESC
LIMIT 1;

快点

根据评论:

I'm trying to select the last message in each of the [...] conversations user 32 is having.

SELECT cu.conversation_id
     , ufrom.username AS from_user
     , uto.username   AS to_user
     , m.message
     , m.send_date
FROM   conversation_user cu
LEFT   JOIN LATERAL (
   SELECT from_user_id, to_user_id, message, send_date
   FROM   message   m
   WHERE  m.conversation_id = cu.conversation_id
   ORDER  BY send_date DESC
   LIMIT  1
   ) m ON TRUE
LEFT   JOIN auth_user ufrom ON ufrom.id = m.from_user_id
LEFT   JOIN auth_user uto   ON uto.id = m.to_user_id
WHERE  cu.user_id = 32;

备注

  • 连接通常比子查询上的 IN 构造更快,尤其是对于大集合。但你也不需要。你把事情搞得太复杂了。

  • 您可以使用 DISTINCT ON 进行更简单的查询,但我希望这个查询更快。
    详情:

    • Select first row in each GROUP BY group?
    • Optimize GROUP BY query to retrieve latest record per user

数据库设计

  • 查询假设 (user_id, conversation_id)UNIQUE - 你 。请务必添加一个实际的 UNIQUE 约束,它会自动提供急需的索引。

  • message(conversation_id, send_date DESC) 的索引也会有所帮助。详情:

  • 假设auth_user.id是PK,所以会被索引

  • message.to_user_uid 可能应该是 to_user_id - 比如 from_user_id.

  • 您可能想添加另一个 FK 以保持一致:

    "message_to_user_id_fkey" FOREIGN KEY (to_user_id) REFERENCES auth_user(id)
    

    不确定您为什么需要 DEFERRABLE INITIALLY DEFERRED。如果您不知道自己需要它,请将其删除。它用于特殊用途,使常规操作更加昂贵。

  • 如果一共去掉conversation_user效率会更高并添加 user1user2 或类似于 conversation - 除非 user/conversation 的每个组合都有更多属性。也可能简化 message。你只需要一个布尔信息而不是 from_userto_user.
    根据关系理论,conversation可以看作是tableauth_user与自身

  • 之间多对多关系的实现