Search code examples
sqlquery-optimizationfirebirdsql-execution-planfirebird-3.0

Query optimization with in (select ...) clause


I'm using Firebird WI-V3.0.4.33054 on Windows.

I'm having trouble optimizing this query, that uses an in clause with a select:

update CADPC p set p.STA = 'L'
where p.COD in (select distinct CODPC from CADPCI_Rec where IDNfr = 27)
and not exists (select * from CADPCI where CODPC = p.COD)

The plan for this query is (and the obvious problem is the P NATURAL part):

PLAN SORT (CADPCI_REC INDEX (PK_CADPCI_REC))
PLAN (CADPCI INDEX (FK_CADPCI_CODPC))
PLAN (P NATURAL)

Select Expression
    -> Filter
        -> Unique Sort (record length: 36, key length: 8)
            -> Filter
                -> Table "CADPCI_REC" Access By ID
                    -> Bitmap
                        -> Index "PK_CADPCI_REC" Range Scan (partial match: 1/3)
Select Expression
    -> Filter
        -> Table "CADPCI" Access By ID
            -> Bitmap
                -> Index "FK_CADPCI_CODPC" Range Scan (full match)
Select Expression
    -> Filter
        -> Table "CADPC" as "P" Full Scan

If, on the other hand, I manually run the select distinct, copy the results and paste in the query, like this:

update CADPC p set p.STA = 'L'
where p.COD in (5699, 5877, 5985)
and not exists (select * from CADPCI where CODPC = p.COD)

Now the optimizer chooses a sensible plan for the P table and the query runs very quickly:

PLAN (CADPCI INDEX (FK_CADPCI_CODPC))
PLAN (P INDEX (PK_CADPC, PK_CADPC, PK_CADPC))

Select Expression
    -> Filter
        -> Table "CADPCI" Access By ID
            -> Bitmap
                -> Index "FK_CADPCI_CODPC" Range Scan (full match)
Select Expression
    -> Filter
        -> Table "CADPC" as "P" Access By ID
            -> Bitmap Or
                -> Bitmap Or
                    -> Bitmap
                        -> Index "PK_CADPC" Unique Scan
                    -> Bitmap
                        -> Index "PK_CADPC" Unique Scan
                -> Bitmap
                    -> Index "PK_CADPC" Unique Scan

I have also tried exists in both conditions, but the result is the same: the subquery is re-evaluated for each row.

update CADPC p set p.STA = 'L'
where exists (select * from CADPCI_Rec where IDNfr = 27 and CODPC = p.COD)
and not exists (select * from CADPCI where CODPC = p.COD)

Plan:

PLAN (CADPCI_REC INDEX (PK_CADPCI_REC))
PLAN (CADPCI INDEX (FK_CADPCI_CODPC))
PLAN (P NATURAL)

Select Expression
    -> Filter
        -> Table "CADPCI_REC" Access By ID
            -> Bitmap
                -> Index "PK_CADPCI_REC" Range Scan (partial match: 1/3)
Select Expression
    -> Filter
        -> Table "CADPCI" Access By ID
            -> Bitmap
                -> Index "FK_CADPCI_CODPC" Range Scan (full match)
Select Expression
    -> Filter
        -> Table "CADPC" as "P" Full Scan

So, the question is: can I somehow make the engine choose the indexed plan when the in clause contains a select (typically only a couple of records)?


Solution

  • You may try EXECUTE BLOCK and FOR SELECT to "invert control".

    Essentially, executing an anonymous ad-hoc procedure

    EXECUTE BLOCK AS
      declare id INTEGER;
    BEGIN
      for select distinct t1.CODPC from CADPCI_Rec t1 
        left join CADPCI t2 on where t2.CODPC = t1.CODPC
      where t2.CODPC is NULL and t1.IDNfr = 27
      into :id
      do 
        update CADPC p set p.STA = 'L' where p.COD = :ID and p.STA <> 'L';
    END
    

    You might also use Global Temporary Tables (GTTs) and then create list of IDs before actually deleting.

    Database preparation (creating a body-less table):

    CREATE GLOBAL TEMPORARY TABLE CADPC_mark_IDs
       ( ID integer )
    ON COMMIT DELETE ROWS
    

    And then the commands would be like

    insert into CADPC_mark_IDs(ID)
    select distinct t1.CODPC from CADPCI_Rec t1 
       left join CADPCI t2 on where t2.CODPC = t1.CODPC
    where t2.CODPC is NULL and t1.IDNfr = 27
    

    then

    update CADPC p set p.STA = 'L'
    where p.COD in (select * from CADPC_mark_IDs) and p.STA <> 'L'
    

    then

    commit; -- clear the in-memory table for next uses
    

    one more option, like Mark suggested, would be using MERGE, after you converted "where not exist" into "left join" (already done above, hopefully correct).

    Something along that

    merge into CADPC p
      using (
        select distinct t1.CODPC as id from CADPCI_Rec t1 
          left join CADPCI t2 on where t2.CODPC = t1.CODPC
        where t2.CODPC is NULL and t1.IDNfr = 27
      ) t
    on (t.id = p.COD) and (p.STA <> 'L')
    when matched then update set p.STA = 'L'