【问题标题】:Is there a way to ensure WHERE clause happens after DISTINCT?有没有办法确保 WHERE 子句在 DISTINCT 之后发生?
【发布时间】:2019-07-08 11:56:23
【问题描述】:

假设您的数据库中有一个表 comments

评论表有idtextshowcomment_id_no列。

如果用户输入评论,它会在数据库中插入一行

| id |  comment_id_no | text | show | inserted_at |
| -- | -------------- | ---- | ---- | ----------- |
| 1  | 1              | hi   | true | 1/1/2000    |

如果用户想要更新该评论,它会在数据库中插入一个新行

| id |  comment_id_no | text | show | inserted_at |
| -- | -------------- | ---- | ---- | ----------- |
| 1  | 1              | hi   | true | 1/1/2000    |
| 2  | 1              | hey  | true | 1/1/2001    |

注意它保持相同的comment_id_no。这样我们就可以查看评论的历史记录。

现在用户决定不再显示他们的评论

| id |  comment_id_no | text | show  | inserted_at |
| -- | -------------- | ---- | ----- | ----------- |
| 1  | 1              | hi   | true  | 1/1/2000    |
| 2  | 1              | hey  | true  | 1/1/2001    |
| 3  | 1              | hey  | false | 1/1/2002    |

这会对最终用户隐藏评论。

现在发表第二条评论(不是第一条的更新)

| id |  comment_id_no | text | show  | inserted_at |
| -- | -------------- | ---- | ----- | ----------- |
| 1  | 1              | hi   | true  | 1/1/2000    |
| 2  | 1              | hey  | true  | 1/1/2001    |
| 3  | 1              | hey  | false | 1/1/2002    |
| 4  | 2              | new  | true  | 1/1/2003    |

我希望能够选择唯一commend_id_no 的所有最新版本,其中show 等于true。但是,我不希望查询返回 id=2

查询需要采取的步骤...

  1. 选择所有最近的、不同的comment_id_nos。 (应该返回 id=3id=4
  2. select where show = true(应该只返回id=4

注意:我实际上是使用 ecto 在 elixir 中编写此查询,并且希望能够在不使用子查询功能的情况下执行此操作。如果有人可以在 sql 中回答这个问题,我可以自己转换答案。如果有人知道如何在长生不老药中回答这个问题,那么也可以随意回答。

【问题讨论】:

  • 请用您正在使用的数据库标记您的问题。
  • @gordon Linoff 我已经更新了标签
  • 我建议不要用历史的东西污染工作表。而不是使用单独的听觉/历史表(最好在单独的模式中)并实现一个简单的触发器(这在 postgres 中非常简单)来记录每个插入、更新(如果允许,甚至删除)操作。这不仅会使您的事情变得更简单,而且还可以避免很多问题……我现在不发布示例,因为它没有回答您的实际问题。但是,如果您对此感兴趣,我可以发布一个示例。
  • TL;DR,在不同之后......你看过HAVING吗?
  • 你不清楚在哪里之前有不同的。写出完整的句子来真正说出你想要的。 distinct 是否在 where 之前有什么关系?--distinct 保持不同的 rows。除非您删除其他列,否则“distinct comment_id_nos”没有意义。也许您正在谈论在子查询中分组和/或选择不同的。但目前还不清楚。您解决了 cmets 中缺乏明确性的问题,但应将说明编辑到帖子中。 PS 这涉及一个甚至有标签的常见问题解答,greatest-n-per-group。您希望从中获得带有 show=true 的行。

标签: sql postgresql elixir distinct where


【解决方案1】:

您可以在不使用 LEFT JOIN 的子查询的情况下执行此操作:

SELECT  c.id, c.comment_id_no, c.text, c.show, c.inserted_at
FROM    Comments AS c
        LEFT JOIN Comments AS c2
            ON c2.comment_id_no = c.comment_id_no
            AND c2.inserted_at > c.inserted_at
WHERE   c2.id IS NULL
AND     c.show = 'true';

我认为所有其他方法都需要某种子查询,这通常可以通过排名函数来完成:

SELECT  c.id, c.comment_id_no, c.text, c.show, c.inserted_at
FROM    (   SELECT  c.id, 
                    c.comment_id_no, 
                    c.text, 
                    c.show, 
                    c.inserted_at,
                    ROW_NUMBER() OVER(PARTITION BY c.comment_id_no 
                                      ORDER BY c.inserted_at DESC) AS RowNumber
            FROM    Comments AS c
        ) AS c
WHERE   c.RowNumber = 1
AND     c.show = 'true';

由于您已使用 Postgresql 进行标记,您还可以使用 DISTINCT ON ():

SELECT  *
FROM    (   SELECT  DISTINCT ON (c.comment_id_no) 
                    c.id, c.comment_id_no, c.text, c.show, c.inserted_at
            FROM    Comments AS c 
            ORDER By c.comment_id_no, inserted_at DESC
        ) x
WHERE   show = 'true';

Examples on DB<>Fiddle

【讨论】:

    【解决方案2】:

    我想你想要:

    select c.*
    from comments c
    where c.inserted_at = (select max(c2.inserted_at)
                           from comments c2
                           where c2.comment_id_no = c.comment_id_no
                          ) and
          c.show = 'true';
    

    我不明白这与select distinct 有什么关系。您只需要最后一个版本的评论,然后检查是否可以显示。

    编辑:

    在 Postgres 中,我会这样做:

    select c.*
    from (select distinct on (comment_id_no) c.*
          from comments c
          order by c.comment_id_no, c.inserted_at desc
         ) c
    where c.show
    

    distinct on 通常具有相当不错的性能特征。

    【讨论】:

    • 我建议 select distinct 的原因是因为我想要一个查询来获取所有显示 = true 的 cmets 的最新版本。我相信如果 show = true,您的答案目前只会得到最后一条评论(如果我错了,请纠正我)我会更新我的问题以使其更清楚。
    • @RobStallion 。 . . SELECT DISTINCT 不会返回最新版本的 anything。它只是保证结果集没有重复的行。
    • @Gordan Linoff。我知道这一点。我想我不够清楚对不起。我的想法是按日期对条目进行排序,然后按不同进行选择。因此 "select all the most recent, distinct comment_id_no..." 在我的问题中。我认为这可以清楚地表明我首先想按日期排序,然后选择不同的值。这就是我返回comment_id_no最新版本 且不重复的方式。你的回答似乎抓住了我的意思。希望这现在很清楚。感谢您的回答和cmets
    • @RobStallion 。 . .也就是说,具有讽刺意味的是,distinct on 做了什么。它特定于 Postgres,但这是您正在使用的数据库。
    【解决方案3】:

    正如我在 cmets 中所说,我不建议用历史/听觉的东西污染数据表。

    不:@Josh_Eller 在他的评论中建议的“双重版本控制”不是 也是很好的解决方案:不仅可以使查询变得不必要,而且还可以 在处理和表空间碎片方面要昂贵得多。

    请记住,UPDATE 操作永远不会更新任何内容。他们反而 编写该行的全新版本并将旧版本标记为已删除。那是 为什么需要真空进程来对表空间进行碎片整理以便 恢复那个空间。

    在任何情况下,除了次优之外,这种方法会迫使您实施更多 读取和写入数据的复杂查询,而事实上,我想大多数时候您只需要选择、插入、更新甚至删除单行,并且最终只需要查看它的历史记录。

    因此,最好的解决方案(恕我直言)是简单地实现您实际需要的架构 完成您的主要任务并在单独的表格中实施听觉 由触发器维护。

    这会更多:

    • 强大而简单:因为您每次都专注于单一事物(Single 责任和 KISS 原则)。

    • 快速: 听觉操作可以在 after 触发器中执行,因此 每次执行 INSERTUPDATEDELETE 任何可能的锁定 因为数据库引擎知道它的结果不会改变,所以事务中的内部还没有被释放。

    • 高效:即更新当然会插入一个新行并标记 旧的已删除。但这将由数据库引擎在低级别完成,不仅如此:您的听觉数据将完全没有碎片(因为您只在那里写:从不更新)。所以整体碎片会少很多。

    话虽如此,如何实现呢?

    假设这个简单的模式:

    create table comments (
        text text,
        mtime timestamp not null default now(),
        id serial primary key
    );
    
    create table comments_audit ( -- Or audit.comments if using separate schema
        text text,
        mtime timestamp not null,
        id integer,
        rev integer not null,
        primary key (id, rev)
    );
    

    ...然后这个函数和触发器:

    create or replace function fn_comments_audit()
    returns trigger
    language plpgsql
    security definer
        -- This allows you to restrict permissions to the auditory table
        -- because the function will be executed by the user who defined
        -- it instead of whom executed the statement which triggered it.
    as $$
    DECLARE
    BEGIN
    
        if TG_OP = 'DELETE' then
            raise exception 'FATAL: Deletion is not allowed for %', TG_TABLE_NAME;
            -- If you want to allow deletion there are a few more decisions to take...
            -- So here I block it for the sake of simplicity ;-)
        end if;
    
        insert into comments_audit (
            text
            , mtime
            , id
            , rev
        ) values (
            NEW.text
            , NEW.mtime
            , NEW.id
            , coalesce (
                (select max(rev) + 1 from comments_audit where id = new.ID)
                , 0
            )
        );
    
        return NULL;
    
    END;
    $$;
    
    create trigger tg_comments_audit
        after insert or update or delete
        on public.comments
        for each row
        execute procedure fn_comments_audit()
    ;
    

    仅此而已。

    请注意,在这种方法中,您将始终拥有当前的 cmets 数据 在 cmets_audit 中。您可以改为使用 OLD 寄存器,并且仅 在 UPDATE(和 DELETE)操作中定义触发器以避免它。

    但我更喜欢这种方法,不仅因为它给了我们额外的冗余(一个 意外删除 - 以防它被允许或意外触发 禁用-在主表上,然后我们将能够从中恢复所有数据 听觉的)而且还因为它简化(和优化)查询 需要时的历史记录。

    现在您只需要以完全透明的方式插入、更新或选择(或者甚至删除,如果您开发更多此模式,即插入带有空值的行...),就像它不是任何一样听觉系统。而且,当您需要这些数据时,只需查询听觉表即可。

    注意:此外,您可能希望包含创建时间戳 (ctime)。在这种情况下,防止它在 BEFORE 触发器中被修改会很有趣,所以我省略了它(再次为了简单起见),因为你已经可以从 mtime em>s 在听觉表中(即使您要在应用程序中使用它,添加它也是非常可取的)。

    【讨论】:

      【解决方案4】:

      如果您运行的是 Postgres 8.4 或更高版本,ROW_NUMBER() 是最有效的解决方案:

      SELECT *
      FROM (
          SELECT c.*, ROW_NUMBER() OVER(PARTITION BY comment_id_no ORDER BY inserted_at DESC) rn
          FROM comments c
          WHERE c.show = 'true'
      ) x WHERE rn = 1
      

      否则,这也可以使用WHERE NOT EXISTS 条件来实现,以确保您显示的是最新评论:

      SELECT c.*
      FROM comments c
      WHERE 
          c.show = 'true '
          AND NOT EXISTS (
              SELECT 1 
              FROM comments c1 
              WHERE c1.comment_id_no = c.comment_id_no AND c1.inserted_at > c.inserted_at
          )
      

      【讨论】:

      • 其实在 Postgres 上,distinct on (..) 通常比窗口函数更高效。
      • @a_horse_with_no_name :感谢您提供信息(以及编辑)!
      【解决方案5】:

      您必须使用group by 来获取最新的 id 并加入 cmets 表以过滤掉 show = false 所在的行:

      select c.* 
      from comments c inner join (
        select comment_id_no, max(id) maxid
        from comments
        group by comment_id_no 
      ) g on g.maxid = c.id
      where c.show = 'true'
      

      我假设 id 列是唯一的并且在 comments 表中自动递增。
      demo

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-08-02
        • 1970-01-01
        • 1970-01-01
        • 2012-03-04
        • 1970-01-01
        • 1970-01-01
        • 2020-07-05
        • 2010-12-12
        相关资源
        最近更新 更多