Skip to content

Speed up the readiness tree's beam destination exclusion - #56

Merged
slominskir merged 1 commit into
mainfrom
readiness-tree-exclusion-performance
Oct 28, 2026
Merged

slominskir merged 1 commit into
mainfrom
readiness-tree-exclusion-performance

Conversation

@slominskir-coding-agent

Copy link
Copy Markdown
Contributor

Follow-up to #53 for #46: the readiness page's tree took 15 to 30 seconds per level with a beam destination excluded, against under a second without one, on a database with 7,000 components in the included destination (the reports from #54 were fine).

The exclusion in filter_system_status, which the tree runs once per system at every level, was a NOT IN subquery inside the function's CASE expression. Oracle cannot rewrite a subquery in that position into a join, so it evaluated it as a filter on every row, and the plan it cached was an index skip scan of the whole component_beam_destination primary key with an LNNVL(component_id <> :B1) filter, about 200 block reads per row. This pull request replaces it with a NOT EXISTS correlated on the component id, which gives the optimizer a unique probe of the primary key per row whatever the statistics say. filter_category_child_table gets the same form for consistency, although Oracle already rewrote its OR form into an indexed NOT EXISTS. The Java side is unchanged.

Upgrade steps

Existing databases need the two create or replace statements below, from container/oracle/initdb.d/02_ddl.sql (a new Oracle container from build.yaml gets them on creation).

SQL for existing databases
create or replace FUNCTION SRM_OWNER.FILTER_SYSTEM_STATUS (sys_id in INTEGER, dest_id_csv in VARCHAR2, excl_dest_id_csv in VARCHAR2, sys_id_csv in VARCHAR2, reg_id in INTEGER, grp_id in INTEGER, stat_id_csv in VARCHAR2)
    RETURN INTEGER
    IS
    retval integer;

    CURSOR c1 IS
        SELECT MAX(a.status_id)
        FROM
            component_status_2 a left join component b on a.component_id = b.component_id
                                 left outer join component_beam_destination c on b.component_id = c.component_id
        WHERE b.system_id = sys_id
          AND b.masked = 'N'
          AND CASE
                  WHEN reg_id IS NULL THEN 1
                  WHEN reg_id IS NOT NULL AND b.region_id = reg_id THEN 1
                  ELSE 0 END = 1
          AND CASE
                  WHEN dest_id_csv IS NULL THEN 1
                  WHEN dest_id_csv IS NOT NULL AND c.beam_destination_id in (select * from table(csv_2_nums(dest_id_csv))) THEN 1
                  ELSE 0 END = 1
          AND CASE
                  WHEN excl_dest_id_csv IS NULL THEN 1
                  WHEN excl_dest_id_csv IS NOT NULL AND NOT EXISTS (select 1 from component_beam_destination x where x.component_id = b.component_id and x.beam_destination_id in (select * from table(csv_2_nums(excl_dest_id_csv)))) THEN 1
                  ELSE 0 END = 1
          AND CASE
                  WHEN sys_id_csv IS NULL THEN 1
                  WHEN sys_id_csv IS NOT NULL AND b.system_id in (select * from table(csv_2_nums(sys_id_csv))) THEN 1
                  ELSE 0 END = 1
          AND CASE
                  WHEN grp_id IS NULL THEN 1
                  WHEN grp_id IS NOT NULL AND a.component_id in (select component_id from component_signoff z where z.group_id = grp_id) THEN 1
                  ELSE 0 END = 1
          AND CASE
                  WHEN stat_id_csv IS NULL THEN 1
                  WHEN stat_id_csv IS NOT NULL AND a.status_id in (select * from table(csv_2_nums(stat_id_csv))) THEN 1
                  ELSE 0 END = 1;
BEGIN
    OPEN c1;
    FETCH c1 INTO retval;

    IF c1%notfound THEN
        retval := 1;
    END IF;

    IF retval IS NULL THEN
        retval := 1;
    END IF;

    CLOSE c1;
    RETURN retval;

EXCEPTION
    WHEN OTHERS THEN
        raise_application_error(-20001,'An error was encountered - '||SQLCODE||' -ERROR- '||SQLERRM);
END;
/
create or replace PROCEDURE SRM_OWNER.FILTER_CATEGORY_CHILD_TABLE (cat_id in integer, dest_id_csv in varchar2, excl_dest_id_csv in varchar2, sys_id_csv in varchar2, reg_id in integer, grp_id in integer, stat_id_csv in varchar2, rtn_cursor out SYS_REFCURSOR) AS
BEGIN
    delete from tmp_component_categories;
    FOR item IN
        (select distinct b.category_id from component a
                                                left join component_status_2 e on a.component_id = e.component_id
                                                left outer join component_beam_destination c on a.component_id = c.component_id,
                                            system b
         where a.system_id = b.system_id
           and a.system_id in (select system_id from system_application where application_id = 1)
           and (reg_id is null or reg_id = a.region_id)
           and (dest_id_csv is null or c.beam_destination_id in (select * from table(csv_2_nums(dest_id_csv))))
           and (excl_dest_id_csv is null or not exists (select 1 from component_beam_destination x where x.component_id = a.component_id and x.beam_destination_id in (select * from table(csv_2_nums(excl_dest_id_csv)))))
           and (sys_id_csv is null or a.system_id in (select * from table(csv_2_nums(sys_id_csv))))
           and (grp_id is null or
                a.component_id in (select component_id from component_signoff z where z.group_id = grp_id))
           and (stat_id_csv is null or
                e.status_id in (select * from table(csv_2_nums(stat_id_csv)))))
        LOOP
            --DBMS_OUTPUT.PUT_LINE('component category: ' || item.category_id);
            FOR thing IN
                (select w.category_id from category w start with w.category_id = item.category_id connect by prior w.parent_id = w.category_id)
                LOOP
                    --DBMS_OUTPUT.PUT_LINE('category heirarchy: ' || thing.category_id);
                    insert into tmp_component_categories values(thing.category_id);
                END LOOP;
        END LOOP;
    open rtn_cursor for select distinct category_id from tmp_component_categories intersect select category_id from category where parent_id = cat_id;
END;
/```

</details>

## Checks

- `./gradlew build`: passes (compile, spotless, the 90 unit tests).
- `./gradlew integrationTest` against the stack from `build.yaml`, with the app rebuilt from this branch and the two routines applied to its Oracle: 49 tests pass (`ReadinessIT`, `StatusReportsIT`, `MaskingRequestIT`, `PagesIT`), including `ReadinessIT`'s checks of the tree, counts and message with destination 13 excluded on the test data.
- Timing, on the dev stack with 7,000 synthetic components added (16 destination memberships each, all but 255 also in the excluded destination), statistics gathered, include destination 9 and exclude 13, `GET /srm/get-children` timed with curl; the synthetic rows and statistics were removed afterwards:

  | Tree request | Before | After |
  |---|---|---|
  | Root node | 14.8 s | 0.1 s |
  | First level (JLAB's children) | 30.3 s | 0.8 s |
  | Second level (CEBAF's children) | 20.6 s | 0.7 s |
  | The same without an exclusion | 0.2 to 0.6 s | 0.1 to 0.3 s |

  Both versions return the same nodes and statuses. Oracle's SQL cache attributed the whole of the slow requests' time to the `filter_system_status` cursor (35,484 executions, 66 s); with the change its plan is an `INDEX UNIQUE SCAN` of `COMPONENT_BEAM_DESTINATION_PK` on both columns, with and without statistics.
- Not run: a browser check, as the change is in the database only and the tree's JSON was checked directly; a regression test for the timing, as CI's data is too small to show it. The CI `build` and CodeQL checks run on this pull request.

## Decisions

- Both routines get the correlated `NOT EXISTS`, although only the function's `CASE` form misbehaved, so the two exclusions read the same and don't depend on the optimizer's rewrite.
- The function's `CASE ... END = 1` style is kept rather than moving the exclusion to an `OR` predicate, to match the surrounding clauses; the `OR` form measured the same.
- The Java criteria queries' top-level `NOT IN` subqueries (system and component children, counts, the reports) are left alone: they were not among the slow statements, and Oracle unnests them into anti-joins.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Prepared-by: Anthropic Claude Code:claude-fable-5-1

The readiness page's tree took 15 to 30 seconds per level with a beam
destination excluded, against under a second without one, on a
database with 7,000 components in the included destination. The
exclusion added to filter_system_status, which the tree runs once per
system at every level, was a NOT IN subquery inside the function's CASE
expression. Oracle cannot rewrite a subquery in that position into a
join, so it evaluated it as a filter on every row, and the plan it
cached for it was an index skip scan of the whole
component_beam_destination primary key with an LNNVL filter: about 200
block reads per row.

Replace it with a NOT EXISTS correlated on the component id, which
gives the optimizer a unique probe of the primary key for each row
whatever the statistics say. filter_category_child_table gets the same
form for consistency, although Oracle already rewrote its OR form well.
Existing databases need the two create or replace statements, given in
the pull request.

Measured on the dev stack with 7,000 synthetic components (16
destination memberships each, all but 255 in the excluded destination)
and gathered statistics, include 9 and exclude 13: the root node went
from 14.8 s to 0.1 s, the first level from 30.3 s to 0.8 s and the
second from 20.6 s to 0.7 s, with the same nodes and statuses. Without
an exclusion the levels take 0.1 to 0.3 s either way.

Part of #46

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@slominskir
slominskir merged commit d3be898 into main Oct 28, 2026
5 checks passed
@slominskir
slominskir deleted the readiness-tree-exclusion-performance branch October 28, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

source::ai Work done by an AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant