SQL: фильтрация строк - PullRequest
       3

SQL: фильтрация строк

6 голосов
/ 11 января 2012

Я пытаюсь написать запрос 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;

Ответы [ 3 ]

4 голосов
/ 11 января 2012

Один из способов - сравнить количество операций.Так как вам также нужно получить количество INSERTS или DELETES, которые соответствуют InsertCount - deleteCount или InsertCount - deleteCount, и поскольку PostgreSQL поддерживает оконную функцию , вы сможете использовать row_number ().

Примечание: я не проверял это так, но в соответствии с этим Руководством по PostgreSQL Глава 3. Расширенные функции, 3.5 Функции окна Вы можете обратиться к функции окна во встроенном запросе

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')
1 голос
/ 12 января 2012

Лучшая скорость и кратчайший ответ: Проблема может быть уменьшена до

  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)
0 голосов
/ 12 января 2012

Тестирование показало, что мой исходный запрос медленнее, чем отличный запрос @ 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 с несколькими упрощениями / улучшениями.Важнейшим моментом является устранение имен, которые были отменены в начале игры.

...