How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/arc-mcp/arc-1 --skill debug-slow-sqlBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Debug Slow SQL / OData
Find the root cause of a slow ABAP SQL or Fiori-Elements OData request and propose a fix — driving ARC-1's diagnostics first (GUI-free), then escalating to SAP GUI / Fiori apps only when the deeper signal needs them. The goal is not "it's slow" but why: which statement, what it scans, and what to change.
Use this when the user reports a slow report/transaction, a slow OData/Fiori list, a long-running CDS view, a timeout/dump under load, or "this query got slow after …".
Inputs (ask only for what's missing)
- What is slow — one of: an OData URL (copy from the browser Network tab), a CDS view / DDLS name, an ABAP
program / class / report, a transaction code, or a table + access pattern.
- How slow / how often — a single slow call vs. slow-under-load vs. intermittent. (Routes you to per-request
vs. aggregate analysis.)
- Reproducible? — can the user re-trigger it on demand (needed to arm a live trace)? On which system
(DEV/QA/PROD) and as which SAP user?
- Recent change? — new code, new data volume, a transport, an index drop. Narrows the search fast.
If you only have a vague "X is slow", get the OData URL or the object name first — everything below keys off it.
The diagnostic ladder — stop at the rung that explains it
Work top-down. Each rung is cheaper than the next and usually tells you whether to descend.
Reachability — don't promise a plan you can't reach. Rungs 3–4 (profiler, ST05) needSAP_ALLOW_WRITESto arm andS_ADT_RES(ACTVT 01 and 02) to read — and don't exist on NW 7.50. If arming returns writes disabled or a403, say so and stop at rung 2:odata_perf+cds_sql+SAPQueryalready pin a DB-bound root cause GUI-free. Tier-3–4 (the exact statement + execution plan) then depends on Basis/config, so hand the user the precise ST05/SAT steps below instead of pretending you reached the plan.
0. Orient (no execution)
SAPContext(action="deps", type="<type>", name=…)/SAPRead(type="DDLS", name=…)— read the CDS/ABAP
source. Eyeball it for the usual suspects before measuring: LIKE '%term%' (leading wildcard = no index), SELECT … FROM with no WHERE on a key, SELECT *, nested SELECT in a LOOP (N+1), client-side filtering, missing FOR ALL ENTRIES pre-check, calculated fields forcing a full scan.
SAPContext(action="impact", type="DDLS", name=…)— the CDS stack (projection → base views → tables). The
slow view is often a thin projection over a heavy base.
- Find the generator, not just the literal SQL. A slow
LIKE '%…%'/scan is often generated — by a search
help, SADL/RAP, or a framework — not hand-written, so a grep/where-used for the literal SELECT comes up empty. Trace the generator instead: for a value-help / type-ahead screen, the search help (DD30L.SELMETHOD + FUZZY_SEARCH, DD32S fields) and its DSH/SADL classes (CL_DSH_*, the F4→WHERE conversion) via SAPSearch/SAPNavigate/SAPRead. For an OData service, find its implementation: a V4/RAP binding → SAPRead(type="SRVB", name=…) → the service definition → the CDS view (then use cds_sql, rung 2). The binding (SRVB) name often differs from the URL's service path — if SRVB 404s, SAPSearch the service name to find the actual binding, or read the service definition (SRVD) directly for its expose … as entities. A classic SEGW / Gateway V2 service has no CDS → find the DPC class (SAPSearch "<SERVICE>_DPC*", read its *_get_entityset / *_get_entity / expand / resolve_navigation_path methods) and skip rung 2. (Don't confuse SE91/WBMESSAGES, which loads one message class in memory and searches with CS, with a search-help path that issues a real DB LIKE.)
1. Where did the time go? (one cheap call)
For an OData / Fiori request:
SAPDiagnose(action="odata_perf", url="/sap/opu/odata4/sap/…/Entity?$filter=…")Read the verdict:
- `db` (
gwappdbdominates) → the CDS/SQL query is the cost → go to rung 2. - `app` (
gwapp≫gwappdb, orgwappdbabsent) → ABAP/SADL logic, not the DB. Do not
ST05-hunt for a slow query — find what implements the service (rung 0) and trace it (rung 3). Classic culprit is an $expand N+1 — isolate it by re-probing the URL with vs without $expand/$select and watch gwapp move.
- `framework` (
gwfw; on 7.50 often onlygwhub) → metadata / first-call / cold cache → re-probe warm.
If only gwhub is present, you do not have a DB/app split yet; descend to a trace if it is not just cold cache.
- `auth` (
icfauth) → ICF/DCL authorization overhead. - `unknown` → no usable Gateway timing at all; confirm the URL is an OData/Gateway service on the SAP host.
(odata_perf needs SAP_ALLOW_DATA_PREVIEW; url must be a host-relative OData path under /sap/opu/odata or /sap/opu/odata4 on the SAP host ARC-1 connects to — other paths are rejected. It does a GET, so use the entity/list request from the Network tab, not a $batch POST.)
2. DB-bound → see and measure the actual SQL
SAPDiagnose(action="cds_sql", name="I_TheView")— the native `CREATE VIEW` the CDS compiles to
(read-only; verified on 7.50/758/816). Now you see the real joins, CASTs, COALESCEs, and whether a sub-view drags in extra tables.
SAPQuery(sql="SELECT … FROM <cds-or-base> WHERE <the filter>")— returnscolumns/rowsplus datapreview
metrics: queryExecutionTimeMs, totalRows (total matches), rowsReturned, and the executedQueryString. Run it with the real filter values from the slow request: a large totalRows / queryExecutionTimeMs for a small useful result = a scan/selectivity problem. It does not expose the HANA execution plan or buffer state — use ST05/HANA for those. A SAPQuery that times out is itself evidence of an unbounded scan — record the timeout as the signal, don't just retry with a smaller maxRows. (Needs SAP_ALLOW_FREE_SQL for freestyle SQL; multi-column WHERE via SAPRead(type="TABLE_QUERY") needs SAP_ALLOW_DATA_PREVIEW.)
- Equality also slow? It's the view, not your filter. If
SAPQuerywith an exactWHERE key = '…'is as
slow as a LIKE '%…%' (both tens of seconds), the LIKE is a red herring — the cost is the CDS itself: a wide SELECT DISTINCT, a deep join the filter can't start from (the filtered field isn't index-leading), or an aggregate / $count=true. cds_sql shows the join order; the fix is to invert the join so the selective table leads, trim the DISTINCT/aggregate out of the list projection, or drop $count=true on a large scan — not to touch the LIKE.
- Compare signals: probe the OData URL, then run
SAPQueryon the underlying CDS/base — ifqueryExecutionTimeMs
is close to the OData gwappdb, the DB query is the cost; if OData is slow but the query is fast, it's the SADL/framework layer above. For the exact statement + execution plan, descend to ST05.
- Recommending a HANA full-text /
CONTAINS/ fuzzy fix? You can't A/B-test it with `SAPQuery` — the ADT
freestyle endpoint is Open-SQL-only and rejects CONTAINS(...) / … CP '*x*' (400 "(" is not allowed). Prove it instead with an ST05 trace during a live reproduce of the real feature, or a one-off ABAP probe report. First confirm the table even has a full-text index: SAPQuery(sql="SELECT indexname, full_text FROM dd12l WHERE sqltab = '<TABLE>'") → full_text = 'X'.
3. App-bound → ABAP profiler trace (which code, which tables)
SAPDiagnose(action="traces") # list recent profiler traces
SAPDiagnose(action="traces", id="<id>", analysis="hitlist") # hottest call paths
SAPDiagnose(action="traces", id="<id>", analysis="dbAccesses") # which tables, counts, buffered?The dbAccesses view tells you which tables a request hit and how often (N+1 shows up as a huge count on one table). The hitlist tells you the ABAP hot path. ARC-1 can arm a profiler trace request itself — SAPDiagnose(action="trace_start", …), then trace_requests to list and trace_cancel to clean up — or record one in SAT/ST12; then list/analyze it with the traces action above.
Two caveats `trace_start` itself flags — heed them: (1) an HTTP trace captures the user's very next matching HTTP call, so after arming make no other ARC-1/MCP calls until the user has reproduced — your own ADT calls will consume the trap. (2) For HTTP/OData traces on HANA the profiler is weak: hitlist/statements are usually empty (or 400 "Data is invalid…"), and business tables hide inside a <DB Access from Kernel> bucket — use dbAccesses only for "did we touch table X, how often", and use ST05 (rung 4) for the actual OData SQL + timings. The profiler is richest for dialog/report/RFC traces of ABAP-side code.
4. The exact SQL + plan → ST05 SQL trace
ARC-1 can arm/disarm the ST05 SQL trace and point you to the records (it can't read the records over ADT — SAP has no SQL-record API; record viewing is the TMC Fiori app / SAP GUI ST05):
SAPDiagnose(action="sql_trace_state") # is a trace already on?
SAPDiagnose(action="set_sql_trace_state", sqlOn=true, user="<SAPUSER>") # arm, filtered to the user (needs SAP_ALLOW_WRITES)
# → user reproduces the slow request now ←
SAPDiagnose(action="sql_trace_directory") # SAP's "SQL Trace Analysis" deep-link
SAPDiagnose(action="set_sql_trace_state", sqlOn=false) # always disarm when doneThen read the records (see "SAP GUI / Fiori escalation"). The record list gives you the exact SELECT, its duration, rows fetched, the object, and (in ST05) the EXPLAIN / execution plan + buffer state. Available on 758/816; not on NW 7.50 (the /st05/trace ADT API returns 404 there — use SAP GUI ST05).
Reading the records (there is no sql_trace_records/sql_trace_read ADT action — sql_trace_directory only returns the viewer link): open the deep-link (TMC "SQL Trace Analysis") or SAP GUI ST05 → Display Trace → pick the app server + your trace file → sort by Duration desc → per row read the SQL text, Duration, Records (rows fetched), Object (table/view); double-click the slowest → Explain for the HANA plan. Look for: extra hits on association/text tables (SADL expansion), repeated identical SELECTs (N+1), or Records ≫ visible rows (selectivity).
If a perf endpoint 403s with "Service cannot be reached", ARC-1 surfaces anicf-service-inactivehint — activate the named SICF node (/sap/bc/stmcfor the trace UI) in tcode SICF.
5. Static check (anytime)
SAPDiagnose(action="atc", type="<type>", name=…, variant="PERFORMANCE_DB") — flags perf anti-patterns statically (it won't catch a runtime LIKE scan that depends on data, but it's free and catches the obvious ones).
SAP GUI / Fiori escalation (when ARC-1's GUI-free signals aren't enough)
ARC-1 is GUI-free up to the point of reading SQL-trace records and execution plans — for those, escalate. Tell the user exactly what to open and what to look for; or, if you have a desktop/Chrome MCP and authorization, drive it yourself (never on PROD without explicit sign-off).
| Tool | Where | What it gives you that ARC-1 can't |
|---|---|---|
| ST05 (SQL/RFC/buffer/enqueue trace) | SAP GUI | The recorded SELECTs with duration + rows; "Explain" → the DB execution plan; identical-/similar-statement grouping; buffer hits. The ground truth for "which statement and why". |
| SQL Trace Analysis | Fiori app (the sql_trace_directory deep-link) | The same ST05 records in a browser (TMC) — use when SAP GUI isn't available. Needs /sap/bc/stmc SICF active. |
| ST12 / SAT | SAP GUI | Combined ABAP+SQL trace with aggregation — best for "where does the time really go" across app+DB in one capture. |
| DBACOCKPIT / HANA | SAP GUI / HANA Studio / DBeaver | EXPLAIN PLAN, PlanViz, M_SQL_PLAN_CACHE, table/index sizes, missing-index hints, optimizer stats freshness. The HANA-side root cause (column-store scan, no pruning, stale stats). |
| ST22 / SM50 / SM66 | SAP GUI | Dumps (e.g. TIME_OUT, TSV_* memory) and what work processes are stuck on under load. |
| SE11 / SE14 | SAP GUI | Indexes on the table, their fields, and whether the slow WHERE matches a usable index prefix. |
| ABAP Cross Trace | ADT (/sap/bc/adt/crosstrace/*, 758+) | RAP/OData-aware cross-layer trace incl. OData V4 request types — the strategic ADT-native record reader (ARC-1 follow-up; not yet a tool). |
Capturing an OData request for `odata_perf`: browser DevTools → Network → click the slow $batch/entity request → copy the path after the host (e.g. /sap/opu/odata4/sap/zsrv/…/Entity?$filter=…&$top=…). That path is the url argument.
Root-cause catalog (pattern → confirm → fix)
| Symptom in the trace/SQL | Likely cause | Confirm | Typical fix |
|---|---|---|---|
Huge rows fetched ≫ rows shown; long duration | Full scan / poor selectivity | cds_sql shows no indexed WHERE; SAPQuery totalRows large / ST05 high rows fetched | Add a WHERE on indexed fields; add a secondary index (SE11); push the filter into the CDS |
LIKE '%term%' | Leading-wildcard = index unusable | Read DDLS/ABAP source | Search help / fuzzy (HANA) / full-text index; anchor the pattern; pre-filter |
| Same table hit thousands of times | N+1 (SELECT in LOOP) | traces dbAccesses shows a giant count on one table | FOR ALL ENTRIES / a join / read-all-then-loop; RAP: prefetch |
SELECT * then use 2 fields | Over-fetch | cds_sql / source | Select only needed fields; trim the CDS projection |
| Fast in DEV, slow in PROD | Data volume / stale stats / different plan | Compare EXPLAIN + table sizes (DBACOCKPIT) | Refresh optimizer stats; add index; partition |
gwappdb small but OData slow | SADL / determinations / virtual elements / auth (DCL) | odata_perf verdict app/auth; profiler hitlist | Move logic to the DB/CDS; simplify DCL; cache; avoid per-row ABAP virtual elements |
| Slow only first call | Metadata / cold cache | odata_perf verdict framework; re-probe warm | Expected; warm-up; don't optimize the query |
TIME_OUT / memory dump under load | Unbounded result / missing paging | ST22 + ST05 | Server-side paging ($top/$skip); add filters; package the work |
Worked example (real, a4h S/4HANA 2023)
A "message-text search screen is slow" complaint. The query behind it:
SAPQuery(sql="SELECT sprsl, arbgb, msgnr, text FROM t100 WHERE text LIKE '%error%'")
→ { totalRows: 12549, queryExecutionTimeMs: 49.086, rowsReturned: 100,
executedQueryString: "SELECT … FROM T100 WHERE TEXT LIKE '%error%' … UP TO 100 ROWS" }Read it: 12 549 rows matched a leading-wildcard LIKE '%…%' on a non-indexed text column → a full table scan (the index is unusable because the pattern starts with %). The result the user actually wanted was a handful of rows. Fix: drop the leading wildcard (anchor the search) / add a search help or full-text index / pre-filter on an indexed field first. For the HANA plan + rows-fetched, arm an ST05 trace (rung 4), reproduce, and open the directory deep-link.
This is the simple case — a single-table scan where the leading wildcard really is the bottleneck. On a wide CDS view it often is not: if an exact-match WHERE key = '…' is just as slow as the LIKE, the cost is the view's joins / DISTINCT / $count, not the wildcard (rung 2, "Equality also slow?"). Measure both before you blame the LIKE.
Output
Deliver a tight diagnosis, not a tool log:
- Verdict — DB / app / framework / auth, with the number that proves it (e.g. "
gwappdb412 ms ofgwtotal
480 ms"). Cite the SAP figure (gwtotal/gwappdb), never wallClockMs: wall-clock also counts network/MCP/proxy (odata_perf returns that gap as clientWaitMs), so when clientWaitMs dwarfs gwtotal the latency is the landscape, not the query.
- The statement — the offending SQL (from
cds_sql/ ST05) and what it scans (SAPQuery totalRows/ ST05
rows fetched, the table, the missing index).
- Root cause — one sentence, mapped to the catalog above.
- Fix — concrete and minimal (the index to add, the filter to push down, the N+1 to collapse), with the
cheapest option first and the trade-off named.
- Evidence — the exact ARC-1 calls run + any ST05/HANA-plan capture, so the user can re-verify.
Always disarm any trace you armed. On PROD, prefer read-only signals (odata_perf, cds_sql, SAPQuery, read-only traces) and get explicit sign-off before arming an ST05 trace or touching state.




