Postgresql - Remove tuple referenced by another table

Asked

Viewed 1,137 times

3

I’m having a problem that I didn’t find anything in Google (maybe I didn’t know to ask him)

Just imagine the following:

  • I have 11 tables, one call tbl_pai and 10 other calls tbl_filha1, tbl_filha2, tbl_daughter{n}.

  • In the daughter tables has a field called id_pai and a Constraint (CONSTRAINT fk_filha_com_pai FOREIGN KEY (id_pai) REFERENCES tbl_pai(id))

That is, 10 tables that have foreign keys referenced to the main table

The question is: How can I elegantly test whether or not I can remove a tuple from the main table? I could only think of two options: Try to delete the parent record and treat the error if the record is referenced in another table, or, make a COUNT in all daughter tables to know if there is dependency.

I need that information to decide whether or not to release a DELETE in my application. If the record has no reference, I display the button, if not I hide the button. The problem is that this button is in a listing (of 'parent' records) and this check needs to be done for each record. I did using the COUNT but was extremely slow as the 'parent' table has over a million records.

There is a less costly method?

Personal thank you.

  • how did you make your Count?? post the Query you had made

  • Okay but I need to stop using Count. In the example above I spoke of imaginary tables, the query I will post is the real one:

  • SELECT 
 (SELECT COUNT(t.id) FROM telemetria t WHERE t.id_sistema = s.id) +
 (SELECT COUNT(a.id) FROM alerta a WHERE a.id_sistema = s.id) + 
 (SELECT COUNT(c.id) FROM controle c WHERE c.id_sistema = s.id) + 
 (SELECT COUNT(cs.id) FROM cliente_sistema cs WHERE s.id_sistema = s.id) AS qtde 
 FROM sistema s 
 WHERE s.id = 1; The telemetry table is very voluminous. The list in the application is systems and I need to check whether or not I provide the button to delete the system.

  • Please take a look at this question: http://dba.stackexchange.com/questions/23041/is-there-a-way-to-test-whether-delete-will-fail-dueto-constraints

5 answers

1


He set up a function for that :

CREATE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION checkDependence(VARCHAR, VARCHAR, INTEGER) RETURNS BOOLEAN AS $$
DECLARE 
    tableName   ALIAS FOR $1;
    columnName  ALIAS FOR $2;
    id          ALIAS FOR $3;

    sqlExecute  RECORD;
    sqlExecute2 RECORD;
    sqlQuery    VARCHAR;
    sqlQuery2   VARCHAR;
    primaryKey  VARCHAR;
BEGIN

    sqlQuery := 'SELECT table_name';
    sqlQuery := sqlQuery || ' FROM information_schema.columns';
    sqlQuery := sqlQuery || ' WHERE table_name != '''||tableName||'''';
    sqlQuery := sqlQuery || ' AND column_name = '''||columnName||'''';

    FOR sqlExecute IN EXECUTE(sqlQuery) LOOP

        RAISE NOTICE '%', sqlExecute.table_name;
        RAISE NOTICE '%', columnName;
        RAISE NOTICE '%', id;

        sqlQuery2 := 'SELECT CASE WHEN count(1) > 0 THEN TRUE ELSE FALSE END AS has';
        sqlQuery2 := sqlQuery2 || ' FROM '||sqlExecute.table_name;
        sqlQuery2 := sqlQuery2 || ' WHERE '||columnName||' = '||id;

        FOR sqlExecute2 IN EXECUTE(sqlQuery2) LOOP
            RAISE NOTICE '%', sqlExecute2;
            IF sqlExecute2.has THEN
                RETURN TRUE;
            END IF;
        END LOOP;
    END LOOP;

    RETURN FALSE;
END;
$$ LANGUAGE plpgsql;

Now you only need to call her in your select :

SELECT
    checkDependence('table_pai', 'id', id);
  • 2

    Dude, perfect! It worked wonderfully well and the reasoning got really smart. The query took 180ms in my database. Thanks for the contribution, so it became extremely modular being possible to use for any table since it follows the correct standard to always give the same name to the foreign keys of the table, as in my case. Great, congratulations! Hugs!

  • You’re welcome @Gustavo, nice of you to help :D

1

Do count can be slow. Solution with the exists:

select *,
    not (
        exists (
            select 1
            from telemetria
            where id_sistema = s.id
        ) or
        exists (
            select 1
            from alerta
            where id_sistema = s.id
        ) or
        exists (
            select 1
            from controle
            where id_sistema = s.id
        ) or
        exists (
            select 1
            from cliente_sistema
            where id_sistema = s.id
        )
    ) as pode_deletar
from sistema s

If you get good performance can also be transformed into function

0

I would do everything in one table, using a field called filhode with a foreign key for the table id itself. I even use this to make menus. See an example:

CREATE TABLE menu
(
  idmenu serial NOT NULL,
  menu character varying(50),
  filhode integer,
  CONSTRAINT pk_idmenu PRIMARY KEY (idmenu),
  CONSTRAINT fk_filhode FOREIGN KEY (filhode)
      REFERENCES menu(idmenu) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
)

With that I don’t need this heap daughters tables like tbl_filha1, tbl_filha2 ... tbl_daughterx. Everything happens in a single table.

0

If the rows of the daughter tables you’re checking aren’t important, I mean, if you just need to consult the existence of daughters referencing the parent you’re trying to exclude, you can use something like:

SELECT 1 FROM tbl_filha1 WHERE id_pai = <id do pai>;

[...]

SELECT 1 FROM tbl_filhaN WHERE id_pai = <id do pai>;

Note that you will have to do this for the N daughter tables (at least until you find a result) which would be a bit onerous. Considering this, in its place I would do the routine for exclusion of the parent table and treat the error in the application if the DBMS bar the exclusion, without prior checking of daughter tables.

  • Your proposal to use Count() may be an option, but you will have to work with all daughter tables, being costly as well.

  • Yes, it is still an option but I will have to make the "Delete" button available for all records in the application. What would be perfect is not to display the button for records that cannot be deleted. As the application is web, I already handled the DBMS error when trying to delete because in the meantime between displaying the data in the application and the user clicking delete, another online user may have generated dependency to the record.

0

Try the following:

SELECT s.*, 

CASE WHEN 
    t.id_sistema IS NOT NULL OR
    c.id_sistema IS NOT NULL OR
    cs.id_sistema IS NOT NULL 
    THEN 'disabled'

END as desabilitar 

FROM sistema s
    LEFT JOIN telemetria t ON s.id = t.id_sistema
    LEFT JOIN controle c ON s.id = c.id_sistema
    LEFT JOIN cliente_sistema cs ON s.id = cs.id_sistema

The code above will list all your systems making a left Join with the other tables. This will cause all independent systems to be listed if there is id_system in the other tables.

CASE WHEN is checking if id_system in the other tables is different from null. If it is not null, it creates the column disable = 'disabled'.

Now in your HTML, in the system listing, use the disable field to assign the disabled to the button.

<button <?= $rs['desabilitar'] ?>>DELETAR</button>

If you are using link tag, change the value of the disabled column to 'None', and then you hide the link with a css inline:

<a style="display:<?=$rs['desabilitar']?>">

This is to avoid the IF. But you can return true and do an if checking to disable = true, hides the button.

But as you said there are millions of records, maybe the best option is to treat the bank exception when you try to delete.

But it is worth trying this solution =)

I hope I’ve helped!

  • Man, thanks for the effort and the contribution. The reasoning is correct but the Johns gave performance problem. The query took 210 seconds on a Linux with i7 and 8GB of ram =) but thanks for the effort. Hugs!

  • William’s solution was really very good, and from what you said, very fast tbm. Wonder!

Browser other questions tagged

You are not signed in. Login or sign up in order to post.