【问题标题】:SQL: filtering rowsSQL:过滤行
【发布时间】:2012-01-11 17:08:10
【问题描述】:

我正在尝试编写一个从包含数据的表中返回行的 SQL 查询:

表结构如下:

CREATE TABLE person(
    id INT PRIMARY KEY,
    name TEXT,
    operation TEXT);

我想返回所有未被“取消”的唯一名称行。 如果操作是“插入”或“删除”,并且存在另一行具有相反操作的同名行,则该行被视为“取消”。

例如,如果我有以下行

id   name   operation
1    bob    insert
2    bob    delete
3    bob    insert

前 2 行相互“取消”,因为它们具有相同的名称和相反的操作。所以查询应该返回第 3 行。

这是另一个例子:

id   name   operation
1    bob    insert
2    bob    delete
3    bob    insert
4    bob    delete

在这种情况下,第 1 行和第 2 行取消,第 3 和第 4 行取消。所以查询不应该返回任何行。

最后一个例子:

id   name   operation
1    bob    insert
2    bob    insert

在这种情况下,第 1 行和第 2 行不会取消,因为操作不是相反的。所以查询应该返回两行。

我有以下查询来处理前两个场景,但它不处理最后一个场景。

有人对可以处理所有 3 种情况的查询有任何建议吗?

SELECT MAX(id),name 
FROM person z 
WHERE operation IN ('insert','delete') 
GROUP BY name 
HAVING count(1) % 2 = 1;

【问题讨论】:

  • 订单重要吗?如果您有INSERT, INSERT, DELETE,应该保留哪个插入?
  • @Martin - 最新的 INSERT 行最相关,所以我猜是第二行。

标签: sql postgresql postgresql-9.1


【解决方案1】:

一种方法是比较操作计数。由于您还需要获取与 InsertCount - deleteCount 或 InsertCount - deleteCount 对应的 INSERTS 或 DELETES 的数量,并且由于 PostgreSQL 支持window function,因此您应该能够使用 row_number()。

注意:我没有对此进行测试,但根据PostgreSQL manual Chapter 3. Advanced Features, 3.5 Window functions,您可以在内联查询中引用窗口函数

SELECT
       id, name
FROM
   (
    SELECT 
            row_number() over (partition by p.name, p.operation order by p.id desc) rn , 
            id,  
            p.Name,
            p.operation, 
            operationCounts.InsertCount,
            operationCounts.deleteCount

    FROM 
       Person p
    INNER JOIN (

        SELECT 
          SUM(CASE WHEN operation = 'insert' then 1 else 0 END) InsertCount,
          SUM(CASE WHEN operation = 'delete' then 1 else 0 END) deleteCount,
          name 
        FROM 
           person 
        GROUP BY
           name ) operationCounts
    ON p.name = operationCounts.name
    WHERE 
      operationCounts.InsertCount <> operationCounts.deleteCount) data
WHERE
      (rn <=  (InsertCount -  deleteCount)
      and operation = 'insert')
      OR
     (rn <=  (deleteCount -  InsertCount)
      and operation = 'delete')

【讨论】:

  • 这很漂亮,但仍然缺少一些东西。你只给max(p.id),你应该给更多(见问题示例3)。
  • 运行查询时出现以下错误:错误:列引用“名称”不明确 LINE 6: row_number() over (partition by name, operation ...
  • @Jin 对此感到抱歉。我忘记了 ROW_NUMBER 中的表别名。我修好了。
  • @Conrad 感谢您的大力帮助。我现在得到一个不同的错误:错误:缺少表“操作计数”第 27 行的 FROM 子句条目:(rn
  • @Jin doh 复制/粘贴错误再次出现。我修好了它。在我们不需要别名引用但如果我们这样做的情况下,它将是data
【解决方案2】:

最佳速度和最短答案: 问题可以简化为

  1. 统计每个名字的删除操作(cnt_del)
  2. 忽略第一个 cnt_del 插入

这可以这样写:(不知道这个查询是否一切正常)

select * from(
    SELECT id, name, 
       row_number() over (partition by name order by case 
                                                     when operation = 'insert' 
                                                     then id 
                                                     else null end 
                                            nulls last ) rnk_insert,
       count(case 
             when operation='delete' then 1 
             else null 
             end) over (partition by name) as cnt_del 
    FROM person z 
    WHERE operation IN ('insert','delete') 
)
where rnk_insert > cnt_del

如果以前在 postgres 中不起作用(AFAIK,Oracle 可以处理),则可以以这种更轻松的方式实施解决方案:

select i.id, i.name 
from

  (select id, name, 
         row_number over (partition by name order by id) as rnk_insert
  from person z
  where operation='insert') i

  left join 

  (select name, count(*) as cnt_del
  from person z 
  where operation='delete') d

  on d.name = i.name

where rnk_insert > coalesce(cnt_del, 0)

【讨论】:

    【解决方案3】:

    测试显示我的原始查询比@Conrad 出色的查询要慢。谦虚地,我尝试了一些方法并提出了一个实际上更简单、更快的查询。

    测试设置

    INSERT INTO person
    SELECT i
          ,'name' || (random() * 500)::int::text
          ,CASE WHEN random() >= 0.5 THEN 'insert' ELSE 'delete' END
    FROM   generate_series(1,10000) AS i;
    

    查询:

    SELECT id, name, operation
    FROM  (
        SELECT row_number() OVER (PARTITION BY name, operation ORDER by id) AS rn
              ,id
              ,name
              ,operation
              ,y.cancel
        FROM  (
           SELECT name
                 ,least(ct_del, ct_all - ct_del) AS cancel
           FROM  (
              SELECT name
                    ,count(*) AS ct_all
                    ,count(NULLIF(operation, 'insert')) AS ct_del
              FROM   person
              GROUP  BY 1
              )   x
           WHERE (ct_all - ct_del) <> ct_del
           )   y
        JOIN   person USING (name)
        )   p
    WHERE  rn > cancel
    

    它最终类似于@Conrad 的查询,但做了一些简化/改进。关键是要消除在游戏早期被取消的名字。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-08-02
      • 2021-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-08
      • 1970-01-01
      相关资源
      最近更新 更多