Search code examples
sqlpostgresqlcommon-table-expressionsql-delete

CTE delete not committed until following statements complete


The problem I'm having is that deleted data still appears later in the same query. Naturally, in a completely separate query, the deleted data does not appear.

This isn't my use-case, but I think this it's the simplest way to show the problem:

CREATE TABLE company (id INT PRIMARY KEY, name TEXT);
CREATE TABLE employee (id INT PRIMARY KEY, company_id INT REFERENCES company(id), name TEXT);

INSERT INTO company VALUES (1, 'first company');
INSERT INTO company VALUES (2, 'second company');

INSERT INTO employee VALUES (1, 1, 'first employee');
INSERT INTO employee VALUES (2, 2, 'second employee');

-- this select can successfully query for the data which has just been deleted
WITH deleted_employee AS (DELETE FROM employee WHERE id = 1 RETURNING id)
SELECT id, name FROM employee JOIN deleted_employee USING (id);

-- this select shows it has been deleted
SELECT * FROM employee;

I've put it into a fiddle here.

It seems the DELETE just isn't committed until the whole query has completed, which feels strange since the precedence requires that the DELETE occurs before the SELECT.

Is there any way to achieve this in a single query?


Edit

The answers have answered the direct problem. The underlying problem is to delete an employee then delete its associated company, if there are no more employees associated with that company.

Here's the query I though would do the trick:

WITH affected_company AS (DELETE FROM employee WHERE id = 1 RETURNING company_id)
DELETE FROM company
USING affected_company
WHERE NOT EXISTS (
  SELECT 1
  FROM employee
  WHERE company_id = affected_company.company_id
);

SELECT * FROM company;
SELECT * FROM employee;

And an updated fiddle.

You can see the company isn't being deleted.


Solution

  • This is expected and documented.

    Quote from the manual

    The sub-statements in WITH are executed concurrently with each other and with the main query. Therefore, when using data-modifying statements in WITH, the order in which the specified updates actually happen is unpredictable. All the statements are executed with the same snapshot (see Chapter 13), so they cannot “see” one another's effects on the target tables. This alleviates the effects of the unpredictability of the actual order of row updates, and means that RETURNING data is the only way to communicate changes between different WITH sub-statements and the main query

    (emphasis mine)


    You can use chained CTEs to delete the company:

    with deleted_emp as (
      delete from employee 
      where id = 1 
      returning company_id, id as employee_id
    )
    delete from company
    where id in (select company_id from deleted_emp) 
      and not exists (select * 
                      from employee e
                         join deleted_emp af 
                           on af.company_id = e.company_id 
                          and e.id <> af.employee_id) 
    

    It's important to exclude the just deleted employee from the not exists sub-query as that will always be visible in the second delete statement and thus the not exists would never be true. So the sub-query essentially checks if there is an employee other than the deleted one assigned to the same company.

    Online example: https://rextester.com/IVZ78695