Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

RETURNING VARCHAR2(4000) PRETTY) AS pretty_json\nFROM orders WHERE order_id = 1;\n```\n\n---\n\n## JSON_EXISTS: Test Path Existence\n\n`JSON_EXISTS` returns TRUE/FALSE (used in WHERE clauses) to test whether a path exists or matches a condition.\n\n```sql\n-- Test for path existence\nSELECT order_id FROM orders\nWHERE JSON_EXISTS(order_data, '$.shipping.tracking_number');\n\n-- Test with filter condition (JSON_EXISTS predicate)\nSELECT order_id FROM orders\nWHERE JSON_EXISTS(order_data, '$.items[*]?(@.price > 100)');\n\n-- Multiple conditions\nSELECT order_id FROM orders\nWHERE JSON_EXISTS(order_data, '$.items[*]?(@.sku == \"WGT-001\" && @.qty >= 2)');\n\n-- Check for null values\nSELECT order_id FROM orders\nWHERE JSON_EXISTS(order_data, '$.status?(@ != null)');\n```\n\n---\n\n## JSON_TABLE: Shred JSON into Relational Rows\n\n`JSON_TABLE` is the most powerful JSON function — it converts a JSON document (or array) into a virtual relational table that can be joined, filtered, and aggregated.\n\n```sql\n-- Expand order items into individual rows\nSELECT o.order_id, o.customer_id, jt.sku, jt.qty, jt.price,\n jt.qty * jt.price AS line_total\nFROM orders o,\n JSON_TABLE(o.order_data, '$.items[*]'\n COLUMNS (\n sku VARCHAR2(20) PATH '$.sku',\n qty NUMBER PATH '$.qty',\n price NUMBER(10,2) PATH '$.price'\n )\n ) jt;\n\n-- Nested JSON_TABLE for hierarchical data\nSELECT o.order_id, jt.method, jt.street, jt.city\nFROM orders o,\n JSON_TABLE(o.order_data, '

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

\n COLUMNS (\n method VARCHAR2(20) PATH '$.shipping.method',\n NESTED PATH '$.shipping.address[*]' COLUMNS (\n street VARCHAR2(100) PATH '$.street',\n city VARCHAR2(50) PATH '$.city',\n zip VARCHAR2(10) PATH '$.zip'\n )\n )\n ) jt;\n\n-- JSON_TABLE with error handling\nSELECT customer_id, jt.item_sku, jt.item_price\nFROM orders,\n JSON_TABLE(order_data, '$.items[*]'\n ERROR ON ERROR -- raise error on malformed JSON\n COLUMNS (\n item_sku VARCHAR2(20) PATH '$.sku' NULL ON ERROR,\n item_price NUMBER(10,2) PATH '$.price' DEFAULT 0 ON ERROR\n )\n ) jt;\n\n-- Aggregate over shredded items\nSELECT o.order_id,\n COUNT(jt.sku) AS item_count,\n SUM(jt.qty * jt.price) AS order_total\nFROM orders o,\n JSON_TABLE(o.order_data, '$.items[*]'\n COLUMNS (\n sku VARCHAR2(20) PATH '$.sku',\n qty NUMBER PATH '$.qty',\n price NUMBER(10,2) PATH '$.price'\n )\n ) jt\nGROUP BY o.order_id;\n```\n\n---\n\n## JSON Modification Functions\n\n```sql\n-- JSON_MERGEPATCH: merge/update a JSON document (RFC 7396)\nUPDATE orders\nSET order_data = JSON_MERGEPATCH(order_data,\n '{\"status\": \"shipped\", \"shipped_at\": \"2025-01-15T10:30:00Z\"}')\nWHERE order_id = 1;\n\n-- JSON_TRANSFORM (21c+): more powerful surgical updates\nUPDATE orders\nSET order_data = JSON_TRANSFORM(order_data,\n SET '$.status' = 'delivered',\n SET '$.delivered_at' = SYSTIMESTAMP FORMAT JSON,\n APPEND '$.tags' = 'completed'\n )\nWHERE order_id = 1;\n\n-- Remove a key\nUPDATE orders\nSET order_data = JSON_TRANSFORM(order_data,\n REMOVE '$.temp_processing_notes')\nWHERE order_id = 1;\n```\n\n---\n\n## JSON Indexes\n\nProper indexing of JSON data is critical for performance. Oracle provides several options.\n\n### Functional Index on JSON_VALUE\n\n```sql\n-- Index on a specific JSON scalar path (most selective, most efficient)\nCREATE INDEX idx_order_status\n ON orders (JSON_VALUE(order_data, '$.status' RETURNING VARCHAR2(20)));\n\nCREATE INDEX idx_order_ship_method\n ON orders (JSON_VALUE(order_data, '$.shipping.method' RETURNING VARCHAR2(20)));\n\n-- These indexes are used automatically by the optimizer\nSELECT order_id FROM orders\nWHERE JSON_VALUE(order_data, '$.status' RETURNING VARCHAR2(20)) = 'pending';\n-- Or with dot notation\nSELECT order_id FROM orders o WHERE o.order_data.status = 'pending';\n```\n\n### JSON Search Index (Oracle Text Full-Text)\n\nFor flexible, multi-path searching across the entire JSON document:\n\n```sql\n-- Creates a full-text index over all JSON content\nCREATE SEARCH INDEX idx_order_json_search ON orders (order_data)\n FOR JSON;\n\n-- Use with JSON_EXISTS\nSELECT order_id FROM orders\nWHERE JSON_EXISTS(order_data, '$.items[*]?(@.sku == \"WGT-001\")');\n\n-- Synchronize the search index (if not SYNC ON COMMIT)\nEXEC CTX_DDL.SYNC_INDEX('idx_order_json_search');\n```\n\n### Composite JSON + Relational Index\n\n```sql\n-- Compound index for common query pattern\nCREATE INDEX idx_cust_status ON orders (\n customer_id,\n JSON_VALUE(order_data, '$.status' RETURNING VARCHAR2(20))\n);\n\n-- Covers: WHERE customer_id = ? AND order_data.status = ?\n```\n\n---\n\n## JSON Duality Views (23c)\n\nJSON Relational Duality Views are one of Oracle 23ai's flagship features, available in 26ai. They expose relational table data as JSON documents that can be fully queried and modified through either a JSON or SQL interface. This eliminates the impedance mismatch between application objects and database rows.\n\n### Creating a Duality View\n\n```sql\n-- Underlying relational tables\nCREATE TABLE customers_23 (\n customer_id NUMBER PRIMARY KEY,\n name VARCHAR2(100),\n email VARCHAR2(200)\n);\n\nCREATE TABLE orders_23 (\n order_id NUMBER PRIMARY KEY,\n customer_id NUMBER REFERENCES customers_23(customer_id),\n status VARCHAR2(20),\n total_amount NUMBER(12,2)\n);\n\nCREATE TABLE order_items_23 (\n item_id NUMBER PRIMARY KEY,\n order_id NUMBER REFERENCES orders_23(order_id),\n sku VARCHAR2(50),\n quantity NUMBER,\n unit_price NUMBER(10,2)\n);\n\n-- JSON Duality View: one JSON document per customer with nested orders\nCREATE OR REPLACE JSON RELATIONAL DUALITY VIEW customer_orders_dv AS\n SELECT JSON {\n 'customerId' : c.customer_id,\n 'name' : c.name,\n 'email' : c.email,\n 'orders' : [\n SELECT JSON {\n 'orderId' : o.order_id,\n 'status' : o.status,\n 'totalAmount' : o.total_amount,\n 'items' : [\n SELECT JSON {\n 'sku' : i.sku,\n 'quantity' : i.quantity,\n 'unitPrice' : i.unit_price\n }\n FROM order_items_23 i WITH (INSERT UPDATE DELETE)\n WHERE i.order_id = o.order_id\n ]\n }\n FROM orders_23 o WITH (INSERT UPDATE DELETE)\n WHERE o.customer_id = c.customer_id\n ]\n }\n FROM customers_23 c WITH (INSERT UPDATE DELETE);\n\n-- Query the duality view as JSON\nSELECT * FROM customer_orders_dv WHERE json_value(data, '$.customerId') = 42;\n\n-- Insert through the duality view (automatically inserts into all tables)\nINSERT INTO customer_orders_dv VALUES (\n '{\"customerId\": 100,\n \"name\": \"Acme Corp\",\n \"email\": \"[email protected]\",\n \"orders\": [\n {\"orderId\": 5001, \"status\": \"pending\", \"totalAmount\": 599.98,\n \"items\": [{\"sku\": \"WGT-001\", \"quantity\": 2, \"unitPrice\": 299.99}]}\n ]}'\n);\n-- This inserts into customers_23, orders_23, AND order_items_23 atomically\n```\n\n---\n\n## Storing vs. Querying JSON: Design Considerations\n\n### When to Store JSON\n\n- **Variable structure**: attributes differ per product category, event type, or customer segment\n- **Schemaless extension**: allow adding fields without schema migrations\n- **Document-oriented data**: configuration objects, API payloads, serialized objects\n- **Nested/array data**: line items, tags, audit trails with natural JSON structure\n\n### When to Normalize Instead\n\n- **Frequently queried scalar fields**: if you always query `$.status`, store it as a column\n- **Referential integrity needed**: foreign keys require relational columns\n- **Aggregation and reporting**: GROUP BY, SUM, AVG on relational columns is faster and clearer\n- **Index selectivity**: B-tree indexes on `NUMBER` columns vastly outperform JSON path indexes\n\n### Hybrid Approach (Most Common)\n\n```sql\n-- Store frequently-queried fields as relational columns\n-- Store variable/extensible attributes as JSON\nCREATE TABLE products (\n product_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n sku VARCHAR2(50) NOT NULL UNIQUE,\n product_name VARCHAR2(200) NOT NULL,\n category_id NUMBER NOT NULL, -- relational FK\n price NUMBER(10,2) NOT NULL, -- indexed, aggregated\n status VARCHAR2(20) DEFAULT 'ACTIVE', -- frequently filtered\n attributes JSON, -- variable: color, size, material, etc.\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP\n);\n\n-- Relational indexes on hot columns\nCREATE INDEX idx_products_category ON products(category_id, status, price);\n\n-- Functional index on common JSON attribute\nCREATE INDEX idx_products_color\n ON products (JSON_VALUE(attributes, '$.color' RETURNING VARCHAR2(50)));\n```\n\n---\n\n## Best Practices\n\n- **Use native JSON type (21c+)** for new schemas. The binary OSON format is significantly faster than BLOB-based storage.\n- **Add IS JSON constraints** on VARCHAR2/BLOB columns in pre-21c databases to validate at insert time.\n- **Create functional indexes on frequently-queried JSON paths** rather than full-text search indexes for single-path queries.\n- **Use JSON_TABLE in FROM clause** rather than JSON_VALUE in SELECT for array expansion — it's set-based and optimizable.\n- **Store scalar values that appear in WHERE clauses as relational columns** with standard indexes. JSON path queries, even with indexes, cannot match the efficiency of a B-tree on a typed column.\n- **Use JSON_MERGEPATCH for document updates** rather than fetching, parsing, modifying, and re-inserting in application code.\n- **Enable `VALIDATE` on JSON Duality Views** to enforce schemas on the JSON side.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Using VARCHAR2 for Large JSON\n\nVARCHAR2 is limited to 32,767 bytes in PL/SQL and 4,000 bytes in SQL (unless `MAX_STRING_SIZE=EXTENDED`). Use BLOB or native JSON for documents that could exceed this.\n\n### Mistake 2: No Index on Queried JSON Paths\n\n```sql\n-- This is a full table scan on every row's JSON\nSELECT * FROM orders WHERE JSON_VALUE(order_data, '$.status') = 'pending';\n\n-- Fix: add a functional index\nCREATE INDEX idx_order_status ON orders(JSON_VALUE(order_data, '$.status' RETURNING VARCHAR2(20)));\n```\n\n### Mistake 3: Parsing JSON in Application Code When SQL/JSON Functions Suffice\n\nDo not fetch the entire JSON document to application code, parse it, extract one field, and return. Use `JSON_VALUE` in the SQL query.\n\n### Mistake 4: Using JSON_QUERY When JSON_VALUE Is Appropriate\n\n`JSON_QUERY` returns a JSON string, even for scalars. For scalar extraction, always use `JSON_VALUE` to get a typed Oracle value.\n\n### Mistake 5: Forgetting Type Conversions in JSON_VALUE\n\n`JSON_VALUE` returns VARCHAR2 by default. Without `RETURNING NUMBER`, numeric comparisons and arithmetic will be wrong or throw implicit conversion errors.\n\n```sql\n-- WRONG: comparing string to number\nWHERE JSON_VALUE(order_data, '$.total') > 100 -- string comparison!\n\n-- RIGHT\nWHERE JSON_VALUE(order_data, '$.total' RETURNING NUMBER) > 100\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c JSON Developer's Guide (ADJSN)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adjsn/)\n- [Oracle Database 19c SQL Language Reference — JSON Functions](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/)\n- [Oracle Database 21c — JSON Data Type](https://docs.oracle.com/en/database/oracle/oracle-database/21/adjsn/)\n- [Oracle Database 26ai — JSON Relational Duality Views](https://docs.oracle.com/en/database/oracle/oracle-database/26/jsnvu/)\n- [Oracle AI Autonomous JSON Database](https://docs.oracle.com/en/cloud/paas/autonomous-json-database/ajdug/work-json-documents-autonomous-database.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18533,"content_sha256":"9bd283436393a9450a470b5244787e7a7ffb3929959e8254e446e435fa092e01"},{"filename":"appdev/locking-concurrency.md","content":"# Locking and Concurrency in Oracle Database\n\n## Overview\n\nOracle's concurrency model is fundamentally different from many other databases. Its Multi-Version Concurrency Control (MVCC) implementation means that **readers never block writers and writers never block readers**. This eliminates a huge class of contention problems that plague other databases, but Oracle still uses locks for write-write conflicts and explicit locking scenarios.\n\nUnderstanding Oracle's locking architecture is essential for writing applications that scale under concurrent load without deadlocks or excessive contention.\n\n---\n\n## Multi-Version Concurrency Control (MVCC)\n\n### How MVCC Works in Oracle\n\nWhen a row is modified, Oracle does not overwrite the old data in place. Instead:\n\n1. The new row version is written to the data block\n2. Instructions on how to resurrect the old row version is stored in the **undo tablespace** (rollback segments)\n3. Readers needing the old version reconstruct it from undo data on demand\n\nThis creates a \"time-travel\" capability: every read sees a **consistent snapshot** of the database at the query's start SCN (System Change Number), regardless of concurrent writers.\n\n```sql\n-- Check current SCN\nSELECT current_scn FROM v$database;\n\n-- Query data as it existed at a specific SCN (Flashback Query)\nSELECT * FROM orders AS OF SCN 12345678;\n\n-- Query as of a timestamp\nSELECT * FROM orders AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '5' MINUTE);\n```\n\n### Read Consistency Guarantees\n\n| Scenario | Oracle Behavior |\n|---|---|\n| Reader vs. Writer (same rows) | No blocking; reader sees pre-change data via undo |\n| Writer vs. Reader (same rows) | No blocking; writer proceeds, reader uses undo |\n| Writer vs. Writer (same row) | Writer 2 blocks until Writer 1 commits or rolls back |\n| Long-running read (undo recycled) | `ORA-01555: snapshot too old` |\n\n### ORA-01555: Snapshot Too Old\n\nThis error occurs when Oracle cannot reconstruct an old row version because the undo data has been overwritten (undo retention exceeded). Prevention strategies:\n\n```sql\n-- Check current undo retention setting\nSHOW PARAMETER undo_retention;\n\n-- Increase undo retention (seconds)\nALTER SYSTEM SET UNDO_RETENTION = 3600; -- 1 hour\n\n-- Check undo advisor recommendation\nSELECT d.undoblks, d.maxquerylen, d.tuned_undoretention\nFROM v$undostat d\nWHERE rownum \u003c= 1;\n\n-- Increase current or potential size of undo tablespace\nALTER DATABASE DATAFILE '\u003cundo file>' RESIZE | AUTOEXEND;\n\n-- Enable undo retention guarantee (prevents undo from being overwritten)\nALTER TABLESPACE undotbs1 RETENTION GUARANTEE;\n```\n\n---\n\n## Row-Level Locking\n\nOracle acquires row-level locks automatically on any row that is `INSERT`ed, `UPDATE`d, or `DELETE`d. These locks are:\n\n- **Exclusive (X mode)**: held by the modifying session\n- **Released only at COMMIT or ROLLBACK**\n- Stored in the data block itself (no lock table), making them essentially free regardless of how many rows are locked\n\n```sql\n-- View current row locks\nSELECT o.object_name, l.session_id, l.locked_mode,\n s.username, s.status, s.sql_id\nFROM v$locked_object l\nJOIN dba_objects o ON l.object_id = o.object_id\nJOIN v$session s ON l.session_id = s.sid\nORDER BY o.object_name;\n```\n\n### Lock Modes\n\n| Mode Code | Name | Description |\n|---|---|---|\n| 0 | None | |\n| 1 | Null (N) | Sub-shared; almost no restriction |\n| 2 | Row Share (SS) | SELECT FOR UPDATE, or DML in progress |\n| 3 | Row Exclusive (SX) | DML in progress on table |\n| 4 | Share (S) | `LOCK TABLE ... IN SHARE MODE` |\n| 5 | Share Row Exclusive (SSX) | |\n| 6 | Exclusive (X) | `LOCK TABLE ... IN EXCLUSIVE MODE`, DDL |\n\n---\n\n## SELECT FOR UPDATE\n\n`SELECT FOR UPDATE` locks selected rows immediately, before any DML is performed. This is the primary mechanism for **pessimistic locking** — reserving rows for update before you have determined the new values.\n\n### Basic Syntax\n\n```sql\n-- Lock all selected rows; wait indefinitely for any already-locked rows\nSELECT account_id, balance\nFROM accounts\nWHERE account_id IN (1001, 2001)\nFOR UPDATE;\n\n-- Lock and process\nDECLARE\n v_balance accounts.balance%TYPE;\nBEGIN\n SELECT balance INTO v_balance\n FROM accounts\n WHERE account_id = 1001\n FOR UPDATE; -- row is now locked exclusively\n\n IF v_balance >= 500 THEN\n UPDATE accounts SET balance = balance - 500 WHERE account_id = 1001;\n UPDATE accounts SET balance = balance + 500 WHERE account_id = 2001;\n COMMIT;\n ELSE\n --\n -- Generally a rollback should be used here, because the PL/SQL error\n -- will automatically roll back changes made in the PL/SQL block, and a rollback\n -- will also roll back any outstanding made BEFORE this block was called which\n -- is typically not the desired behaviour\n --\n --ROLLBACK;\n --\n RAISE_APPLICATION_ERROR(-20001, 'Insufficient funds');\n END IF;\nEND;\n```\n\n### NOWAIT — Fail Immediately If Locked\n\n```sql\n-- Raise ORA-00054 immediately if any row is already locked\nSELECT product_id, stock_qty\nFROM inventory\nWHERE product_id = 42\nFOR UPDATE NOWAIT;\n\n-- Handle in application\nDECLARE\n v_qty NUMBER;\nBEGIN\n BEGIN\n SELECT stock_qty INTO v_qty FROM inventory WHERE product_id = 42 FOR UPDATE NOWAIT;\n EXCEPTION\n WHEN resource_busy THEN -- ORA-00054\n RAISE_APPLICATION_ERROR(-20002, 'Product is being updated by another user. Please try again.');\n END;\n\n IF v_qty > 0 THEN\n UPDATE inventory SET stock_qty = stock_qty - 1 WHERE product_id = 42;\n COMMIT;\n END IF;\nEND;\n```\n\n### WAIT n — Wait Up to N Seconds\n\n```sql\n-- Wait up to 5 seconds for the lock; then raise ORA-30006\nSELECT order_id, status\nFROM orders\nWHERE order_id = 9999\nFOR UPDATE WAIT 5;\n```\n\n### SKIP LOCKED — Non-Blocking Queue Processing\n\n`SKIP LOCKED` is extremely useful for implementing work queues. It skips any rows that are already locked rather than waiting, allowing multiple workers to process the queue in parallel without contention.\n\n```sql\n-- Worker process: claim the next available pending job\nDECLARE\n l_job_id NUMBER;\n l_payload VARCHAR2(4000);\n l_rc SYS_REFCURSOR;\nBEGIN\n open l_rc for\n SELECT job_id, payload\n FROM job_queue\n WHERE status = 'PENDING'\n ORDER BY created_at\n FOR UPDATE SKIP LOCKED;\n\n FETCH l_rc INTO l_job_id, l_payload;\n EXIT WHEN l_rc%NOTFOUND;\n\n -- Mark as in-progress\n UPDATE job_queue SET status = 'PROCESSING', started_at = SYSTIMESTAMP\n WHERE job_id = l_job_id;\n\n COMMIT;\n\n -- Process the job (outside the lock)\n process_job(l_job_id, l_payload);\n\n -- Mark complete\n UPDATE job_queue SET status = 'DONE', completed_at = SYSTIMESTAMP\n WHERE job_id = l_job_id;\n COMMIT;\n\nEXCEPTION\n WHEN NO_DATA_FOUND THEN\n NULL; -- No jobs available\nEND;\n```\n\nwill not produce correct behaviour. It will fetch PENDING one row (which may already be locked by another process) and only then attempt to lock it, and subsequently skip it, thus returning no rows.\n\nMultiple instances of this worker can run concurrently without any inter-process coordination — Oracle handles the row-level locking automatically.\n\nA common mistake when using SKIP LOCKED is using ROWNUM to limit the returned row. For example\n\n```sql\n SELECT job_id, payload INTO l_job_id, l_payload\n FROM job_queue\n WHERE status = 'PENDING'\n AND ROWNUM = 1\n FOR UPDATE SKIP LOCKED;\n```\n\n---\n\n## Deadlock Detection and Avoidance\n\nA **deadlock** occurs when two or more sessions are each waiting for a lock held by another session in the cycle.\n\nOracle automatically detects deadlocks using a background cycle-detection algorithm. When detected:\n- One session receives `ORA-00060: deadlock detected while waiting for resource`\n- Oracle rolls back **only the statement** that received the error (not the entire transaction)\n- The rolled-back session must re-execute the statement or roll back the transaction\n\n```sql\n-- Deadlock scenario\n-- Session 1 Session 2\nUPDATE t SET v=1 WHERE id=1; -- OK\n UPDATE t SET v=2 WHERE id=2; -- OK\nUPDATE t SET v=3 WHERE id=2; -- WAITS\n UPDATE t SET v=4 WHERE id=1; -- WAITS -> DEADLOCK\n -- Session 2 receives ORA-00060\n```\n\n### Deadlock Alert Log\n\nOracle records deadlock traces in the alert log and a trace file:\n\n```bash\n# Find deadlock traces\ngrep -l \"deadlock\" $ORACLE_BASE/diag/rdbms/*/trace/*.trc | tail -5\n```\n\n```sql\n-- Check recent deadlocks in unified auditing / alert log\nSELECT value FROM v$diag_info WHERE name = 'Default Trace File';\n```\n\n### Deadlock Avoidance Strategies\n\n**Strategy 1: Consistent Lock Ordering**\n\nAlways acquire locks in the same order across all code paths:\n\n```sql\n-- WRONG: different order creates deadlock potential\n-- Path A: locks order 1, then order 2\n-- Path B: locks order 2, then order 1\n\n-- RIGHT: always lock in ascending order\n-- Both paths: lock lower ID first, then higher ID\nSELECT * FROM orders WHERE order_id IN (1, 2) ORDER BY order_id FOR UPDATE;\n```\n\n**Strategy 2: Lock at the Start of a Transaction**\n\nAcquire all needed locks upfront rather than incrementally:\n\n```sql\n-- Lock all rows the transaction will need before doing any computation\nSELECT account_id, balance\nFROM accounts\nWHERE account_id IN (:from_acct, :to_acct)\nORDER BY account_id -- consistent ordering\nFOR UPDATE;\n```\n\n**Strategy 3: Use NOWAIT / Short WAIT**\n\nConvert waiting deadlocks into immediately-handled exceptions:\n\n```sql\nBEGIN\n SELECT * FROM resource_table WHERE resource_id = :id FOR UPDATE NOWAIT;\n -- ... process ...\n COMMIT;\nEXCEPTION\n WHEN resource_busy THEN\n -- Retry after brief delay, or queue the work\n log_retry('Resource busy, retrying...');\n DBMS_SESSION.SLEEP(0.5);\n -- retry logic\nEND;\n```\n\n**Strategy 4: Minimize Transaction Duration**\n\nThe longer a transaction holds locks, the more opportunity for deadlocks.\n\n---\n\n## Table Locks\n\nOracle acquires **table-level locks (TM locks)** in addition to row locks. Table locks prevent conflicting DDL while DML is in progress. They do NOT prevent concurrent DML unless explicitly escalated.\n\n### Explicit Table Locking\n\n```sql\n-- Lock entire table to prevent concurrent modifications\n-- (blocks other DML; use sparingly)\nLOCK TABLE orders IN EXCLUSIVE MODE;\nLOCK TABLE orders IN EXCLUSIVE MODE NOWAIT; -- fail if locked\n\n-- Share mode: prevents DML but allows concurrent readers\nLOCK TABLE orders IN SHARE MODE;\n\n-- Row exclusive: default mode acquired automatically during DML\nLOCK TABLE orders IN ROW EXCLUSIVE MODE;\n```\n\n### When Table Locks Are Needed\n\nTable locks in `EXCLUSIVE MODE` are rarely needed in application code. Primary use cases:\n- Bulk load operations where you want to prevent any concurrent DML\n- The database may lock a table during schema changes when ONLINE DDL is not available\n- Explicit synchronization for ETL processes\n\n```sql\n-- ETL pattern: lock staging table exclusively for safe swap\nBEGIN\n LOCK TABLE staging_orders IN EXCLUSIVE MODE NOWAIT;\n\n -- Merge staging into production\n MERGE INTO production_orders p\n USING staging_orders s ON (p.order_id = s.order_id)\n WHEN MATCHED THEN UPDATE SET p.status = s.status\n WHEN NOT MATCHED THEN INSERT VALUES (s.order_id, s.status, s.created_at);\n\n DELETE FROM staging_orders;\n COMMIT;\nEXCEPTION\n WHEN resource_busy THEN\n RAISE_APPLICATION_ERROR(-20010, 'Staging table is locked; ETL already running?');\nEND;\n```\n\n---\n\n## Lock Monitoring Queries\n\n### Active Locks and Blocked Sessions\n\n```sql\n-- Find blocking sessions and what they are blocking\nSELECT\n blocker.sid AS blocking_sid,\n blocker.serial# AS blocking_serial,\n blocker.username AS blocking_user,\n blocker.status AS blocking_status,\n blocker.sql_id AS blocking_sql_id,\n waiter.sid AS waiting_sid,\n waiter.username AS waiting_user,\n waiter.event AS waiting_event,\n waiter.wait_time_micro / 1e6 AS wait_seconds,\n obj.object_name AS locked_object,\n obj.object_type\nFROM\n v$session blocker\n JOIN v$lock bl ON bl.sid = blocker.sid AND bl.block = 1\n JOIN v$lock wl ON wl.id1 = bl.id1 AND wl.id2 = bl.id2\n AND wl.request > 0\n JOIN v$session waiter ON waiter.sid = wl.sid\n LEFT JOIN dba_objects obj ON obj.object_id = bl.id1\nORDER BY\n wait_seconds DESC;\n```\n\n### Lock Wait Tree (Hierarchical)\n\n```sql\n-- Show the full lock wait chain using hierarchical query\nSELECT\n LPAD(' ', 2 * (LEVEL - 1)) || sid AS sid,\n username,\n status,\n osuser,\n machine,\n program,\n blocking_session,\n wait_class,\n event,\n seconds_in_wait\nFROM\n v$session\nWHERE\n status = 'ACTIVE'\n OR blocking_session IS NOT NULL\nCONNECT BY PRIOR sid = blocking_session\nSTART WITH blocking_session IS NULL AND status = 'ACTIVE'\nORDER SIBLINGS BY sid;\n```\n\n### Identify SQL Being Executed by Blocked Session\n\n```sql\nSELECT s.sid, s.blocking_session, s.event,\n sq.sql_text, s.seconds_in_wait\nFROM v$session s\nJOIN v$sql sq ON s.sql_id = sq.sql_id\nWHERE s.blocking_session IS NOT NULL;\n```\n\n### Lock History (AWR — requires Diagnostics Pack license)\n\n```sql\n-- Top waiting events for locks over last hour\nSELECT event, total_waits, time_waited_micro / 1e6 AS total_wait_secs\nFROM v$system_event\nWHERE wait_class = 'Concurrency'\nORDER BY time_waited_micro DESC;\n```\n\n---\n\n## Optimistic vs. Pessimistic Locking\n\n### Pessimistic Locking (SELECT FOR UPDATE)\n\nLock the row immediately, before reading the value you'll base your update on. Use when:\n- Contention on the row is high\n- You cannot afford to retry on conflict\n- The \"think time\" between read and update is very short\n\n### Optimistic Locking\n\nRead the row without locking. Only at update time, verify the row hasn't changed:\n\n```sql\n-- Read (no lock)\nSELECT order_id, status, last_modified\nINTO :order_id, :status, :last_modified\nFROM orders\nWHERE order_id = 1001;\n-- Application processes the data, user thinks about it...\n\n-- Update with collision detection using comparison with values previously fetched\nUPDATE orders\nSET status = 'APPROVED', last_modified = SYSTIMESTAMP\nWHERE order_id = 1001\n AND last_modified = :last_modified; -- fails if row was changed since we read it\n\nIF SQL%ROWCOUNT = 0 THEN\n -- Row was modified by someone else; retry or raise conflict error\n RAISE_APPLICATION_ERROR(-20003, 'Conflict: order was modified. Please reload and retry.');\nEND IF;\nCOMMIT;\n```\n\nAn anti-pattern is to use the ORA_ROWSCN function as a mechanism for optimistic locking. Avoid this because\n- Tables require the ROW DEPENDENCIES clause, but no error will be raised if this is not the case\n- ORA_ROWSCN does not work on Index Organized Tables\n- ORA_ROWSCN requires columns in the UPDATE-SET clause are also referenced as predicates in the WHERE clause, otherwise it can return null\n\nIn version 26ai an above, the SYS_ROW_ETAG can be used for optimistic locking without the drawbacks of ORA_ROWSCN\n\n**Using a version column for optimistic locking:**\n\n```sql\n-- Table design\nCREATE TABLE orders (\n order_id NUMBER PRIMARY KEY,\n status VARCHAR2(20),\n version_no NUMBER DEFAULT 1 NOT NULL -- incremented on every update\n);\n\n-- Update with version check\nUPDATE orders\nSET status = :new_status,\n version_no = version_no + 1\nWHERE order_id = :order_id\n AND version_no = :read_version; -- must match what was read\n\nIF SQL%ROWCOUNT = 0 THEN\n RAISE_APPLICATION_ERROR(-20004, 'Stale data: please reload.');\nEND IF;\n```\n\n---\n\n## Best Practices\n\n- **Prefer optimistic locking** for low-contention scenarios. Only escalate to `FOR UPDATE` when you genuinely need to guarantee no concurrent modification between read and update.\n- **Keep lock duration as short as possible.** Acquire locks immediately before the DML, not at the start of a user interaction.\n- **Never hold locks across network round-trips or user input.** A user who goes to lunch while your transaction holds a lock blocks everyone else.\n- **Use `SKIP LOCKED` for queue-based workloads** to enable horizontal scaling of workers without a separate queue infrastructure.\n- **Order lock acquisitions consistently** to prevent deadlocks.\n- **Monitor `v$lock` and `v$session`** in production for blocking chains. Set up alerting when `seconds_in_wait` exceeds a threshold.\n- **Avoid `LOCK TABLE IN EXCLUSIVE MODE`** in application code — it is almost always the wrong tool and creates a serialization bottleneck.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Assuming Reads Are Blocked by Writes\n\nDevelopers from SQL Server or MySQL backgrounds sometimes add unnecessary `NOLOCK` hints or read-uncommitted isolation. In Oracle, this is never needed — reads are never blocked by writers.\n\n### Mistake 2: Catching ORA-00060 and Ignoring It\n\nWhen an application catches a deadlock error, it must roll back the statement (Oracle already did the statement rollback, but the transaction is still open with prior changes) and then decide whether to retry or abort the whole transaction.\n\n```plpgsql\n-- WRONG: continue as if nothing happened\nEXCEPTION WHEN OTHERS THEN\n IF SQLCODE = -60 THEN NULL; END IF; -- ignore deadlock!\n\n-- RIGHT: rollback and handle\nEXCEPTION WHEN OTHERS THEN\n IF SQLCODE = -60 THEN\n ROLLBACK;\n retry_or_raise();\n ELSE\n ROLLBACK;\n RAISE;\n END IF;\n```\n\n### Mistake 3: Using SELECT FOR UPDATE in Read-Only Scenarios\n\n`SELECT FOR UPDATE` acquires exclusive locks. If the application only reads the data (no subsequent DML), those locks block other writers unnecessarily for the duration of the transaction.\n\n### Mistake 4: Escalating to Table Locks Prematurely\n\nSome developers use `LOCK TABLE IN EXCLUSIVE MODE` to \"be safe\" during batch updates. This serializes all processing, destroying any benefit from parallel execution. Use row-level locking and batch commits instead.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c Concepts (CNCPT) — Data Concurrency and Consistency](https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/)\n- [Oracle Database 19c Application Developer's Guide (ADFNS)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/)\n- [V$LOCK — Oracle Database 19c Reference](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-LOCK.html)\n- [V$SESSION — Oracle Database 19c Reference](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-SESSION.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19109,"content_sha256":"9db34d0cb4d0375723dc7273d0bf1af040f176448c1132050a6bfd895dc1d849"},{"filename":"appdev/mle-javascript-in-database.md","content":"# Multilingual Engine JavaScript in the Database\n\n## Overview\n\nOracle AI Database Multilingual Engine (MLE) lets you run and store JavaScript directly in the database. According to the Oracle AI Database JavaScript Developer's Guide for 26ai, MLE supports JavaScript for:\n\n- Stored procedures\n- Stored functions (UDF)\n- Code in a PL/SQL package namespace\n- Anonymous, dynamic code snippets executed through `DBMS_MLE`\n\nMLE JavaScript is supported when connecting with a dedicated server connection on Linux x86-64 or Linux for Arm (aarch64), including dedicated server connections that use Database Resident Connection Pooling (DRCP). Shared server connections cannot use MLE.\n\nThe JavaScript implementation is compliant with ECMAScript 2023. The runtime requires Linux x86-64 or Linux aarch64. MLE is not available for other platforms. Dynamic MLE execution using `dbms_mle` is available for Oracle Database 21c on Linux x86-64. Oracle 19c and earlier don't ship with MLE at all.\n\nIn addition to JavaScript it is possible to use Typescript with the database, too. Before Typescript code can be loaded it must be transpiled to JavaScript. Typescript type declarations for MLE are available on [GitHub](https://github.com/oracle-samples/mle-modules)\n\nMLE/JavaScript is a \"pure\" JavaScript implementation of the ECMAScript 2023 standard. Node specific modules and many browser APIs are not available. \u003chttps://oracle-samples.github.io/mle-modules/> contains a list of built-in modules. Direct Typescript support is not available; a transpilation step is always required.\n\n---\n\n## Core Building Blocks\n\n### MLE Modules\n\nUse `CREATE MLE MODULE` to persist JavaScript source code as a schema object.\n\n```sql\nCREATE OR REPLACE MLE MODULE hello_world_module\nLANGUAGE JAVASCRIPT AS\n\n export function greet(str){\n console.log(`Hello, ${str}`);\n }\n/\n```\n\nKey points from the docs:\n\n- Module names must be unique within the schema.\n- ECMAScript syntax must be used.\n- MLE objects share namespace rules with tables, views, sequences, private synonyms, PL/SQL packages, functions, procedures, and cache groups.\n- `CREATE OR REPLACE MLE MODULE` preserves previously granted privileges.\n- `IF NOT EXISTS` is supported and is mutually exclusive with `OR REPLACE`.\n- If JavaScript source has syntax errors, the module is still created but exists in an invalid state.\n- Modules are expected to be UTF-8 encoded\n- When creating MLE modules, you should create JavaScript files to simplify integration with the existing toolchain. JavaScript files can be loaded into the database using SQLcl's `mle create-module` command. This is the preferred way to avoid encoding issues and work around size limitations.\n- JavaScript code should be preserved either as a `.js` or `.ts` file\n- Modules cannot be used with Edition Based Redefinition. In cases where multiple editions are required a PL/SQL call specification must be created for the new edition\n- Modules can be home-grown/self-written or alternatively be downloaded from 3rd party repositories\n- A 3rd party's module dependencies are best resolved using bundlers such as esbuild, rollup, etc.\n\n### MLE Environments\n\nUse `CREATE MLE ENV` to define import mappings and JavaScript language options.\n\n```sql\nCREATE OR REPLACE MLE ENV po_env\nIMPORTS (\n 'po_module' MODULE PO_MODULE\n);\n```\n\nEnvironment basics:\n\n- Environments are schema objects stored in the data dictionary.\n- `CREATE MLE ENV IF NOT EXISTS myEnv;` is supported.\n- `CREATE MLE ENV MyEnvDuplicate CLONE MyEnv` creates a point-in-time copy.\n- Import mappings cannot conflict with built-in module names such as `mle-js-oracledb`, `mle-js-bindings`, `mle-js-plsqltypes`, `mle-js-fetch`, `mle-encode-base64`, `mle-js-encodings`, and `mle-js-plsql-ffi`.\n- `CREATE MLE ENV` fails if an imported module does not exist or is not accessible.\n\n### Call Specifications\n\nUse PL/SQL `CREATE FUNCTION ... AS MLE MODULE ...` or `CREATE PROCEDURE ... AS MLE MODULE ...` to publish JavaScript exports to SQL and PL/SQL.\n\n```sql\nCREATE OR REPLACE PROCEDURE\n GREET(str in VARCHAR2)\n AS MLE MODULE jsmodule\n SIGNATURE 'greet(string)';\n/\n\nCREATE OR REPLACE FUNCTION CONCATENATE(str1 in VARCHAR2, str2 in VARCHAR2)\n RETURN VARCHAR2\n AS MLE MODULE jsmodule\n SIGNATURE 'concat(string, string)';\n/\n```\n\nImportant behavior:\n\n- The `MLE MODULE` clause requires the module to be in the same schema as the call specification.\n- The optional `ENV` clause selects an environment; if omitted, the default environment is used.\n- The `SIGNATURE` clause maps the PL/SQL entry point to the exported JavaScript function.\n- The `SIGNATURE` clause can omit parameter types, using default PL/SQL-to-MLE type mappings.\n- `AUTHID CURRENT_USER` and `AUTHID DEFINER` behave like the same clauses on PL/SQL units.\n- `AUTHID` defaults to definer's rights when omitted.\n\n### Inline JavaScript functions and procedures\n\nInline MLE call specifications embed JavaScript code directly in `CREATE FUNCTION` and `CREATE PROCEDURE` DDLs, so you can define the JavaScript logic right in the database object instead of deploying a separate module. They are best for simple, one-off functionality, while module-based call specifications are better for larger or reusable code.\n\n```sql\nCREATE OR REPLACE FUNCTION date_to_epoch (\n \"theDate\" TIMESTAMP WITH TIME ZONE\n)\nRETURN NUMBER\nAS MLE LANGUAGE JAVASCRIPT\n{{\n const d = new Date(theDate);\n\n //check if the input parameter turns out to be an invalid date\n if (isNaN(d)){\n throw new Error(`${theDate} is not a valid date`);\n }\n\n //Date.prototype.getTime() returns the number of milliseconds\n //for a given date since epoch, which is defined as midnight\n //on January 1, 1970, UTC\n return d.getTime();\n}};\n/\n```\n\nImportant behavior:\n\n- Use `MLE LANGUAGE JAVASCRIPT` to mark the function or procedure as JavaScript, and wrap the JavaScript body in matching delimiters such as `{{ ... }}`. The delimiters must match and cannot be reserved words or a dot.\n- Use `MLE LANGUAGE JAVASCRIPT PURE` to disable access to the database\n- PL/SQL arguments are converted automatically to JavaScript types and passed into the JavaScript body. Parameter names are mapped to JavaScript names, and unquoted names become uppercase.\n- For functions, the JavaScript return value is converted back to the PL/SQL return type. Procedures do not have a return value.\n- Syntax is checked at compile time. If the JavaScript has errors, the object can still be created but remains invalid until fixed.\n- Inline call specs support `OR REPLACE`, `IF NOT EXISTS`, schema qualification, parameter declarations, `AUTHID`, and `PURE`. `DETERMINISTIC` applies only to functions.\n- Inline call specs cannot import MLE modules, built-in or custom. Instead, they use prepopulated JavaScript global variables to access built-in module functionality.\n\nWhen to use inline vs. module-based call specs\n\nInline call specs are the quick option for a single JavaScript function or procedure with minimal setup. Module-based call specs are more flexible when you need imports, reuse, or more complex code. Every variable defined in the global scope can be use in inline functions/procedures. External modules cannot.\n\n### Handling of PL/SQL OUT and IN OUT Parameters in MLE/JavaScript\n\nOracle MLE JavaScript functions support `OUT` and `IN OUT` parameters, similar to PL/SQL call specifications. In JavaScript, these parameters are passed as wrapper objects, so the function must read and update the underlying value through the `value` property rather than using pass-by-reference directly. The call specification uses `InOut\u003cT>` or `Out\u003cT>` in the `SIGNATURE`, and `DBMS_MLE` does not use these wrappers.\n\n---\n\n## Dynamic JavaScript Execution with `DBMS_MLE`\n\nDynamic execution is the alternative to storing code in MLE modules. Oracle documents that JavaScript code can be provided inline as `VARCHAR2` or as `CLOB` for larger snippets, then executed through the `DBMS_MLE` PL/SQL package.\n\n```sql\nSET SERVEROUTPUT ON;\nDECLARE\n l_ctx dbms_mle.context_handle_t;\n l_snippet CLOB;\nBEGIN\n l_ctx := dbms_mle.create_context(environment => 'PURE_ENV');\n l_snippet := q'~\n console.log('Hello World, this is dynamic MLE execution');\n ~';\n dbms_mle.eval(l_ctx, 'JAVASCRIPT', l_snippet);\n dbms_mle.drop_context(l_ctx);\nEXCEPTION\n WHEN OTHERS THEN\n dbms_mle.drop_context(l_ctx);\n RAISE;\nEND;\n/\n```\n\nDocumented characteristics:\n\n- `DBMS_MLE` allows data exchange between PL/SQL and JavaScript.\n- JavaScript can execute PL/SQL through built-in JavaScript modules.\n- Dynamic execution mixes PL/SQL setup and JavaScript runtime execution.\n- `EXECUTE DYNAMIC MLE` is required before a user can use `DBMS_MLE`.\n- Applications should not use `DBMS_MLE` directly, modules/environments and inline call specs are preferred\n- Use DBMS_MLE to allow a developer to test a snippet\n\n---\n\n## Using the MLE JavaScript SQL Driver\n\nThe built-in `mle-js-oracledb` driver is modeled after `node-oracledb`, but Oracle documents several important differences. The module doesn't need to be imported, since the `session` object, allowing database interaction, is available in the global scope. This session variable is equivalent to node-oracledb's `const result = connection.execute()` construct. Since all MLE/JavaScript code runs inside the database there is no need to establish a connection as you would in node-oracledb.\n\n```sql\ncreate or replace mle module purchase_order_module\nlanguage javascript as\n\n/**\n * A simple function accepting a purchase Order (as per chapter 4 in the Oracle Database\n * JSON Developer's Guide) and checking whether its value is high enough to merit the\n * addition of a free item\n *\n * @param {object} po the purchase order to be checked\n * @param {object} freeItem which item to add to the order free of charge\n * @param {number} threshold the minimum order value before a free item can be added\n * @param {boolean} a flag indicating whether the free item was successfully added\n * @returns {object} the potentially updated purchaseOrder\n * @throws exceptions in case\n * - any of the mandatory parameters is null\n * - in the absence of line items\n * - if the free item has already been added to the order\n */\nexport function addFreeItem(po, freeItem, threshold, itemAdded) {\n\n // sanity checking\n if (po == null || freeItem == null || threshold == null) {\n throw new Error(`mandatory parameter either not provided or null`);\n }\n\n // ensure there are line items provided by the purchase order\n if (po.LineItems === undefined) {\n throw new Error(`PO number ${po.PONumber} does not contain any line items`);\n }\n\n // bail out if the free Item has already been added to the purchase order\n if (po.LineItems.find(({ Part }) => Part.Description === freeItem.Part.Description)) {\n throw new Error(`${freeItem.Part.Description} has already been added to order ${po.PONumber}`);\n }\n\n // In, Out, and InOut Parameters are implemented using special interfaces, see\n // https://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/and-parameters.html\n itemAdded.value = false;\n\n // get the total order value\n const poValue = po.LineItems\n .map(x => x.Part.UnitPrice * x.Quantity)\n .reduce(\n (accumulator, currentValue) => accumulator + currentValue, 0\n );\n \n // add a free item to the purchase order if its value\n // exceeds the threshold\n if ( poValue > threshold ) {\n\n // update the ItemNumber\n freeItem.ItemNumber = (po.LineItems.length + 1)\n po.LineItems.push(freeItem);\n itemAdded.value = true;\n }\n\n return po;\n}\n/\n```\n\nThe guide also documents globally available symbols for MLE JavaScript SQL driver code:\n\n- `oracledb`\n- `session`\n- `soda`\n- `plsffi`\n- `OracleNumber`\n- `OracleClob`\n- `OracleBlob`\n- `OracleTimestamp`\n- `OracleTimestampTZ`\n- `OracleDate`\n- `OracleIntervalDayToSecond`\n- `OracleIntervalYearToMonth`\n\nKey differences from `node-oracledb`:\n\n- The API is synchronous by default.\n- A promise-based interface is not provided.\n- Use `oracledb.defaultConnection()` to get the current session connection.\n- `oracledb.createConnection`, connection pools, and `connection.close()` are not supported.\n- `connection.execute()`, `connection.executeMany()`, `connection.commit()`, and `connection.rollback()` return results or throw exceptions instead of using callbacks or promises.\n- The auto-commit feature is not implemented; if specified on `connection.execute()`, the parameter is ignored.\n\nUnsupported SQL driver data types documented by Oracle:\n\n- `LONG`\n- `LONG RAW`\n- `XMLType`\n- `BFILE`\n- `REF CURSOR`\n\nAdditional documented features not available in the MLE JavaScript SQL driver:\n\n- `connection.break`\n- `connection.createLob`\n- `property oracledb.lobPrefetchSize`\n- `constant oracledb.BLOB`\n- `constant oracledb.CLOB`\n- `connection.queryStream()`\n- `resultSet.toQueryStream()`\n- Continuous Query Notification (CQN)\n- `Connection.subscribe()`\n- `Connection.unsubscribe()`\n- All Continuous Query Notification constants in the `oracledb` class\n- All Subscription constants in the `oracledb` class\n\n---\n\n## Working with SODA Collections in MLE JavaScript\n\nOracle documents SODA for MLE JavaScript as the in-database document API for create, read, update, and delete operations on collections. When using the MLE JavaScript SQL driver, the global `soda` symbol is already available and represents a `SodaDatabase`, so most code can work directly with collections without extra connection setup. If you need the explicit driver flow, `oracledb.defaultConnection().getSodaDatabase()` returns the same starting point.\n\n```javascript\n/**\n * Retrieve a purchase order by ID\n * @param {number} id the purchase order's ID\n * @returns {object} the purchase order as retrieved from the database\n * @throws an exception if the collection cannot be opened for reading\n */\nexport function getPurchaseOrder(id) {\n const collection = soda.openCollection(\"exampleCollection\");\n \n if (collection === null) {\n throw new Error(\"failed to open the collection for reading\");\n }\n \n const data = collection.find().filter({ _id: id }).getOne().getContent();\n \n return data;\n}\n \n/**\n * Process a purchase order\n * @param {object} po the purchase order to process\n * @throws an exception if the validation fails\n */\nexport function savePurchaseOrder(po) {\n \n const collection = soda.createCollection(\"exampleCollection\");\n \n if (collection === null) {\n throw new Error(\"failed to open the collection for reading\");\n }\n \n // simulate some kind of validation. Assume, for example,\n // you need at least one line item and a customer name\n // this would be more sophisticated in real life.\n if (\"customer\" in po && po.lineItems.length > 0) {\n \n collection.insertOne(po);\n } else {\n throw new Error(\"error storing the purchase order: document failed validation\");\n }\n}\n```\n\nDocumented SODA capabilities:\n\n- `soda.createCollection()` creates a collection and opens it if one with the same name already exists.\n- Collection names are case-sensitive.\n- Unless custom metadata is provided, the default collection metadata is used. By default, collections store JSON documents and automatically generate keys and version information.\n- `soda.openCollection()` returns `null` when a collection does not exist instead of throwing an error.\n- `soda.getCollectionNames()` can list all collections, optionally limited with parameters such as `limit` and `startsWith`.\n- `soda.createDocument()` can create a `SodaDocument`, but Oracle positions `insertOne()` and `insertOneAndGet()` as the common path for most applications.\n- `insertOne()` inserts a document, while `insertOneAndGet()` also returns generated metadata such as key and version, but not the document content.\n- `save()` and `saveAndGet()` behave like insert operations unless a client-assigned key already exists, in which case the existing document is replaced.\n- `find()` returns a `SodaOperation` used through method chaining. Common nonterminal operations include `key()`, `keys()`, `filter()`, `skip()`, `limit()`, and `version()`.\n- Common terminal operations include `getOne()`, `getCursor()`, `count()`, `replaceOne()`, `replaceOneAndGet()`, and `remove()`.\n- Query-by-example filters (`filter()`) support structured JSON searches, including comparisons and logical operators.\n- `replaceOne()` requires a document selected by key. Oracle documents that using `save()` is different because `save()` can insert when the target key does not already exist.\n- `remove()` returns a result object with a `count` field so you can verify how many documents were deleted.\n- `createIndex()` and `dropIndex()` manage collection indexes. Oracle documents B-tree indexes for specific JSON fields and JSON search indexes for ad hoc search and full-text search.\n- `getDataGuide()` returns a `SodaDocument` describing the current JSON structure of a collection, but it requires a JSON-only collection with a JSON search index configured with `\"dataguide\": \"on\"`.\n- `SodaDocumentCursor` objects returned by `getCursor()` must be closed to free resources.\n- SODA transactions follow the same rules as the MLE JavaScript SQL driver: there is no `autoCommit`, so changes must be committed or rolled back explicitly.\n- `SodaCollection.drop()` drops the collection and its metadata. Oracle explicitly warns not to drop the underlying table with SQL because that leaves SODA metadata behind.\n- Oracle documents both inline call specifications and module-based call specifications for invoking SODA-enabled JavaScript from SQL and PL/SQL.\n\n---\n\n## Security and Privileges\n\nOracle separates privileges for dynamic execution from privileges for creating schema objects.\n\n### Documented Privileges\n\n```sql\nGRANT EXECUTE DYNAMIC MLE TO \u003crole | user>\nGRANT CREATE MLE TO \u003crole | user>\nGRANT CREATE PROCEDURE TO \u003crole | user>\nGRANT CREATE ANY MLE TO \u003crole | user>\nGRANT DROP ANY MLE TO \u003crole | user>\nGRANT ALTER ANY MLE TO \u003crole | user>\nGRANT CREATE ANY PROCEDURE TO \u003crole | user>\nGRANT COLLECT DEBUG INFO ON \u003cmodule> TO \u003crole | user>\n```\n\nDocumented notes:\n\n- `EXECUTE DYNAMIC MLE` is required for `DBMS_MLE` dynamic execution.\n- `CREATE MLE` is required to create modules and environments in your own schema.\n- `CREATE PROCEDURE` is also required if you expose JavaScript through call specifications.\n- For SODA usage in MLE JavaScript, Oracle documents granting `SODA_APP`.\n- `CREATE ANY MLE`, `DROP ANY MLE`, and `ALTER ANY MLE` are powerful privileges intended only for trusted users.\n- Starting with Oracle AI Database 26ai, Oracle documents `DB_DEVELOPER_ROLE` as a quick way to grant common developer privileges in local development databases.\n- For development databases ONLY, it's possible to grant all required privileges using the `db_developer_role`\n\n### `MLE_PROG_LANGUAGES`\n\nOracle documents the initialization parameter `MLE_PROG_LANGUAGES` with values `ALL`, `JAVASCRIPT`, or `OFF`.\n\nDocumented behavior:\n\n- It can be set at CDB, PDB, or session level.\n- If disabled at CDB level, it cannot be enabled at PDB or session level.\n- If disabled at PDB level, it cannot be enabled at session level.\n- In 26ai, MLE supports JavaScript as its sole language, so `ALL` and `JAVASCRIPT` have the same effect.\n- Setting it to `OFF` prevents execution of JavaScript code, but does not prevent creation or modification of existing code.\n\n---\n\n## Execution Contexts and `PURE`\n\nMLE uses execution contexts to isolate runtime state such as global variables. Oracle documents:\n\n- Session state does not outlive the database session.\n- State can be discarded with `DBMS_SESSION.reset_package()`.\n- Context reuse depends on the MLE module, environment, and invoking user.\n- Separate execution contexts are created to prevent information leakage and unwanted side effects.\n\nFor restricted execution, use `PURE`. When using MLE modules an appropriate MLE Env is required:\n\n```sql\nCREATE OR REPLACE MLE ENV pure_env\nIMPORTS( 'pure_mod' MODULE pure_mod) PURE;\n\nCREATE OR REPLACE PROCEDURE helloWorld\nAS \nMLE MODULE pure_mod\nENV pure_env\nSIGNATURE 'helloWorld';\n/\n```\n\nFor inline call specifications you can add the PURE keyword directly:\n\n```sql\ncreate or replace function epoch_to_date(\n \"p_epoch\" number\n) return date\nas mle language javascript PURE\n{{\n let d = new Date(0);\n d.setUTCSeconds(p_epoch);\n return d;\n}};\n/\n```\n\nYou can also specify the PURE keyword for dynamic MLE:\n\n```sql\ndeclare\n l_ctx dbms_mle.context_handle_t;\n l_snippet clob;\nbegin\n -- to specify pure execution with DBMS_MLE, make sure you use\n -- an MLE environment that has been created with the pure keyword\n l_ctx := dbms_mle.create_context(\n environment => 'PURE_ENV'\n );\n l_snippet := q'~\n \n(async () => {\n \n const { string2JSON } = await import ('common');\n \n console.log(JSON.stringify(string2JSON('a=1;b=2')));\n \n}) ()\n \n ~';\n dbms_mle.eval(l_ctx, 'JAVASCRIPT', l_snippet);\n dbms_mle.drop_context(l_ctx);\nexception\n when others then\n dbms_mle.drop_context(l_ctx);\n raise;\nend;\n/\n```\n\nOracle documents that `PURE`:\n\n- Disallows access to stateful database APIs inside JavaScript, in particular the SQL driver.\n- Can be specified on MLE environments, inline call specifications, and used through `DBMS_MLE` via a `PURE` environment.\n- Still allows interaction through function arguments, outputs, and exported symbols.\n- Still allows supported data types, including reference types such as LOBs passed into MLE.\n\nJavaScript APIs and globals Oracle says are unavailable during `PURE` execution:\n\n- `mle-js-oracledb`\n- `mle-js-plsql-ffi`\n- `mle-js-fetch`\n- `session`\n- `soda`\n- `plsffi`\n- `oracledb`\n- `require`\n\nOracle also documents that APIs not interacting with database state, such as `mle-js-plsqltypes` and `mle-js-encodings`, remain accessible in `PURE` execution.\n\n---\n\n## Dictionary Views\n\nOracle documents these dictionary view families for inspecting MLE metadata:\n\n```sql\nSELECT line, text\nFROM USER_SOURCE\nWHERE name = 'PO_MODULE';\n\nSELECT MODULE_NAME, VERSION, METADATA\nFROM USER_MLE_MODULES\nWHERE LANGUAGE_NAME='JAVASCRIPT'\n/\n\nSELECT ENV_NAME, LANGUAGE_OPTIONS\nFROM USER_MLE_ENVS\nWHERE ENV_NAME='MYENVOPT'\n/\n\nSELECT IMPORT_NAME, MODULE_OWNER, MODULE_NAME\nFROM USER_MLE_ENV_IMPORTS\nWHERE ENV_NAME='MYFACTORIALENV';\n/\n```\n\nUse these view families as documented:\n\n- `[USER | ALL | DBA | CDB]_SOURCE`\n- `[USER | ALL | DBA | CDB]_MLE_MODULES`\n- `[USER | ALL | DBA | CDB]_MLE_ENVS`\n- `[USER | ALL | DBA | CDB]_MLE_ENV_IMPORTS`\n\n---\n\n## Best Practices\n\nBased on the Oracle documentation:\n\n- Prefer dedicated server connections; MLE is not supported with shared server.\n- Use the PL/SQL Foreign Function Interface (plsffi) whenever you call PL/SQL.\n- Always use and require ECMAScript syntax/modules.\n- Use modules plus call specifications for persistent, reusable database APIs.\n- Suggest the use of bundlers such as esbuild for complex third party modules with a large dependency tree.\n- Use environments to manage import resolution and JavaScript language options.\n- Run linting and code analysis before `CREATE MLE MODULE`; the docs explicitly recommend established JavaScript tooling and CI/CD validation. Start by creating the JavaScript file, apply linting and other best practices. Use SQLcl's `mle create-module` command to load the javascript file into the database, except for inline call specifications.\n- inline call specifications are always to be created as a SQL file\n- Use bind variables in SQL executed from JavaScript.\n- Grant the minimum privileges necessary, especially for `ANY` privileges.\n- Use `PURE` for third-party or computational code that should not access database state.\n- Use wrapper types such as `oracledb.ORACLE_NUMBER` or `fetchInfo` when precision loss matters.\n- use the foreign function interface (plsffi) when invoking PL/SQL for a better developer experience.\n- Do not use the SODA API unless explicitly requested by the developer.\n\n## Common Mistakes\n\n- Expecting MLE to work with shared server connections.\n- Assuming client-driver patterns like connection pools, `connection.close()`, promises, or query streams are available in `mle-js-oracledb`.\n- Assuming all node APIs are available. Network and file system access in particular are limited by design and need to be enabled.\n- Forgetting that `AUTHID` defaults to definer's rights on call specifications.\n- Omitting `CREATE PROCEDURE` or `CREATE FUNCTION` when creating call specifications that expose JavaScript.\n- Using `PURE` and then attempting to access `session`, `oracledb`, `soda`, or `plsffi`.\n- Assuming `autoCommit` works like `node-oracledb`; Oracle documents that the flag is ignored.\n- Assuming all Oracle data types are supported by the MLE SQL driver.\n- Using CommonJS modules instead of ECMAScript modules — only ECMAScript is supported.\n- MLE is available on Linux x86-64 and aarch64 only. It runs natively on Linux, and can be used on MacOS via container engines. Windows users should use WSL2.\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Oracle AI Database 26ai has a dedicated JavaScript Developer's Guide for MLE and documents JavaScript as the supported MLE language.\n- The fetched 26ai documentation explicitly documents MLE JavaScript modules, environments, call specifications, dynamic execution via `DBMS_MLE`, the `mle-js-oracledb` SQL driver, `PURE` execution, and the `MLE_PROG_LANGUAGES` parameter.\n- The fetched 26ai table of contents also shows release-update history for 23.x documentation, indicating MLE JavaScript content exists in later Oracle AI Database releases before 26ai.\n- Oracle Database 19c does not support MLE at all.\n- Oracle Database 21c only supports dynamic JavaScript execution via `DBMS_MLE`. It does not support modules, environments, or the ECMAScript syntax\n\n## Sources\n\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/overview-multilingual-engine-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/toc.htm>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/23/mlejs/index.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/system-and-object-privileges-required-working-javascript-mle.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/creating-call-specification-mle-module.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/dynamic-javascript-execution.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/security-considerations-mle.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/introduction-javascript-mle-sql-driver.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/api-differences-node-oracledb-and-mle-js-oracledb.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/soda-collections-in-mle-js.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/getting-started-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/creating-document-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/opening-existing-document-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/discovering-existing-collections-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/creating-documents-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/inserting-documents-collections-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/saving-documents-collections-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/finding-documents-collections-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/replacing-documents-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/removing-documents-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/indexing-documents-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/getting-data-guide-collection-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/handling-transactions-soda-database-javascript.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/creating-call-specifications-involving-soda-api.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/managing-javascript-modules-database.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/creating-mle-environments-database.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/restricted-execution-contexts.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/dictionary-views-related-mle-javascript-modules.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/dictionary-views-related-mle-javascript-environments.html>\n- \u003chttps://docs.oracle.com/en/database/oracle/oracle-database/26/mlejs/and-parameters.html>\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":29560,"content_sha256":"d51b55bb6262b5acf98b80d7785df82b228bee622f9ea6f9cb7d9c7b6c0d38ee"},{"filename":"appdev/nodejs-oracledb.md","content":"# Node.js + Oracle Database\n\n## Overview\n\n`node-oracledb` is the official Oracle driver for Node.js. Like `python-oracledb`, it supports two modes:\n\n- **Thin mode** (default): pure JavaScript, no Oracle Client required. Supports most features.\n- **Thick mode**: requires Oracle Instant Client. Needed for Advanced Queuing, Sharding, and some proxy auth scenarios.\n\nUnless a snippet shows its own wrapper, JavaScript examples below assume they are running inside an `async` function and that `conn` is an open connection acquired earlier.\n\n```bash\nnpm install oracledb\n```\n\n---\n\n## Connecting\n\n### Basic Connection (Promise API)\n\n```js\nconst oracledb = require('oracledb');\n\n// Default to Promise-based API\noracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; // return rows as objects\n\nasync function run() {\n let conn;\n try {\n conn = await oracledb.getConnection({\n user: 'hr',\n password: 'password',\n connectString: 'localhost:1521/freepdb1' // Easy Connect\n });\n\n const result = await conn.execute('SELECT sysdate AS now FROM dual');\n console.log(result.rows[0].NOW);\n } finally {\n if (conn) await conn.close();\n }\n}\n\nrun();\n```\n\n### TNS Alias\n\n```js\nconst conn = await oracledb.getConnection({\n user: 'hr',\n password: 'password',\n connectString: 'mydb_high' // alias from tnsnames.ora\n});\n```\n\n### Wallet / mTLS (Autonomous Database)\n\n```js\nconst conn = await oracledb.getConnection({\n user: 'admin',\n password: 'password',\n connectString: 'myatp_high',\n configDir: '/path/to/wallet',\n walletLocation: '/path/to/wallet',\n walletPassword: 'walletpassword'\n});\n```\n\n### Enabling Thick Mode\n\n```js\n// Must be called before any connection\noracledb.initOracleClient({ libDir: '/opt/oracle/instantclient_21_9' });\n```\n\n---\n\n## Executing SQL\n\n### Bind Variables\n\n```js\n// Named binds (recommended)\nconst result = await conn.execute(\n `SELECT last_name, salary\n FROM employees\n WHERE department_id = :deptId AND salary > :minSal`,\n { deptId: 60, minSal: 5000 }\n);\nconsole.log(result.rows);\n\n// Positional binds\nconst result2 = await conn.execute(\n 'SELECT last_name FROM employees WHERE employee_id = :1',\n [100]\n);\n```\n\n### DML with Binds\n\n```js\nconst result = await conn.execute(\n 'UPDATE employees SET salary = :sal WHERE employee_id = :id',\n { sal: 9500, id: 100 },\n { autoCommit: true }\n);\nconsole.log(`Rows updated: ${result.rowsAffected}`);\n```\n\n### Batch Execution (executeMany)\n\n```js\nconst rows = [\n { id: 201, name: 'Alice', dept: 10 },\n { id: 202, name: 'Bob', dept: 20 },\n { id: 203, name: 'Carol', dept: 10 },\n];\n\nconst result = await conn.executeMany(\n 'INSERT INTO employees (employee_id, last_name, department_id) VALUES (:id, :name, :dept)',\n rows,\n { autoCommit: true }\n);\nconsole.log(`Rows inserted: ${result.rowsAffected}`);\n```\n\n---\n\n## Fetching Results\n\n```js\n// Default: rows as arrays\noracledb.outFormat = oracledb.OUT_FORMAT_ARRAY;\nconst r1 = await conn.execute('SELECT employee_id, last_name FROM employees WHERE rownum \u003c= 5');\nconsole.log(r1.rows); // [[100, 'King'], [101, 'Kochhar'], ...]\n\n// Rows as objects (easier to work with)\noracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;\nconst r2 = await conn.execute('SELECT employee_id, last_name FROM employees WHERE rownum \u003c= 5');\nconsole.log(r2.rows); // [{ EMPLOYEE_ID: 100, LAST_NAME: 'King' }, ...]\n\n// Limit rows returned (maxRows)\nconst r3 = await conn.execute(\n 'SELECT * FROM employees',\n [],\n { maxRows: 100 }\n);\n```\n\n### Result Sets (for Large Queries)\n\n```js\nconst result = await conn.execute(\n 'SELECT employee_id, last_name, salary FROM employees',\n [],\n { resultSet: true, fetchArraySize: 100 }\n);\n\nconst rs = result.resultSet;\nlet row;\nwhile ((row = await rs.getRow()) !== null) {\n console.log(row);\n}\nawait rs.close();\n```\n\n### Query Stream\n\n```js\nconst stream = conn.queryStream(\n 'SELECT employee_id, last_name FROM employees',\n [],\n { fetchArraySize: 200 }\n);\n\nstream.on('metadata', meta => console.log(meta));\nstream.on('data', row => console.log(row));\nstream.on('end', () => stream.destroy());\nstream.on('close', () => console.log('done'));\nstream.on('error', err => console.error(err));\n```\n\n---\n\n## Connection Pooling\n\n```js\n// Create pool at startup\nawait oracledb.createPool({\n user: 'hr',\n password: 'password',\n connectString: 'localhost:1521/freepdb1',\n poolMin: 2,\n poolMax: 10,\n poolIncrement: 1,\n poolAlias: 'default' // optional name\n});\n\n// Acquire from pool\nasync function queryEmployees() {\n let conn;\n try {\n conn = await oracledb.getConnection(); // gets from default pool\n const result = await conn.execute('SELECT COUNT(*) AS cnt FROM employees');\n return result.rows[0].CNT;\n } finally {\n if (conn) await conn.close(); // returns to pool, does not close\n }\n}\n\n// Close pool at shutdown\nawait oracledb.getPool().close(10); // 10s drain timeout\n```\n\n---\n\n## PL/SQL Calls\n\n```js\n// Stored procedure with IN/OUT\nconst result = await conn.execute(\n `BEGIN hr.get_employee(:id, :name, :sal); END;`,\n {\n id: { val: 100, dir: oracledb.BIND_IN, type: oracledb.NUMBER },\n name: { val: '', dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 100 },\n sal: { val: 0, dir: oracledb.BIND_OUT, type: oracledb.NUMBER }\n }\n);\nconsole.log(result.outBinds.name, result.outBinds.sal);\n\n// REF CURSOR\nconst result2 = await conn.execute(\n `BEGIN OPEN :rc FOR SELECT employee_id, last_name FROM employees WHERE department_id = :dept; END;`,\n {\n rc: { dir: oracledb.BIND_OUT, type: oracledb.CURSOR },\n dept: 60\n }\n);\nconst rs = result2.outBinds.rc;\nconst rows = await rs.getRows(100);\nawait rs.close();\nconsole.log(rows);\n```\n\n---\n\n## LOB Handling\n\n```js\n// Read CLOB as string (small LOBs)\noracledb.fetchAsString = [oracledb.CLOB];\n\nconst result = await conn.execute(\n 'SELECT resume FROM employee_docs WHERE employee_id = :id',\n { id: 100 }\n);\nconsole.log(result.rows[0].RESUME); // string\n\n// Read BLOB as Buffer (small BLOBs)\noracledb.fetchAsBuffer = [oracledb.BLOB];\n\nconst r2 = await conn.execute(\n 'SELECT photo FROM employee_photos WHERE employee_id = :id',\n { id: 100 }\n);\nconst buf = r2.rows[0].PHOTO; // Buffer\nrequire('fs').writeFileSync('photo.jpg', buf);\n\n// Write CLOB\nconst text = 'Large document content...';\nawait conn.execute(\n 'UPDATE employee_docs SET resume = :resume WHERE employee_id = :id',\n { resume: text, id: 100 },\n { autoCommit: true }\n);\n```\n\n---\n\n## Best Practices\n\n- **Always use bind variables** — never template literals with user data in SQL.\n- **Use a connection pool** in Express/Fastify/Koa apps — create once at startup, close at shutdown.\n- **Set `oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT`** globally for cleaner code.\n- **Use `executeMany`** for bulk DML instead of looping `execute`.\n- **Use result sets or streams** for large queries instead of `maxRows`.\n- **Return connections to the pool** in a `finally` block — always call `conn.close()`.\n- **Use `autoCommit: true`** only for single-statement transactions; manage commits explicitly otherwise.\n\n---\n\n## Common Mistakes\n\n| Mistake | Problem | Fix |\n|---------|---------|-----|\n| Interpolating user input into SQL | SQL injection | Use named or positional binds |\n| Not calling `conn.close()` in `finally` | Pool exhaustion | Always close in finally |\n| `maxRows` default (0 = unlimited) | Can OOM on large tables | Set `maxRows` or use result sets |\n| Forgetting `await` on async calls | Silent failures | Ensure all async calls are awaited |\n| `fetchAsString`/`fetchAsBuffer` not set for LOBs | LOB objects need manual streaming | Set globally or per-query |\n| Creating a new connection per request | Connection overhead; pool not used | Use `oracledb.createPool` at startup |\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Thin mode supports Oracle Database 12.1 and later.\n- `node-oracledb` 6.x adds thin mode and improved JSON support for 21c+.\n- Oracle 23ai `VECTOR` type is supported via `oracledb.DB_TYPE_VECTOR` in node-oracledb 6.4+.\n\n## Sources\n\n- [node-oracledb Documentation](https://node-oracledb.readthedocs.io/)\n- [node-oracledb GitHub](https://github.com/oracle/node-oracledb)\n- [Oracle Node.js Developer Center](https://www.oracle.com/database/technologies/appdev/nodejs.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8332,"content_sha256":"c61611e4534154d5c48f6039a9d1d495be95b6f6d6b9631974b2af5e9e15217e"},{"filename":"appdev/oracle-text.md","content":"# Oracle Text: Full-Text Search\n\n## Overview\n\nOracle Text (formerly ConText and interMedia Text) is Oracle's full-text search engine, built into the database kernel. Unlike application-level search libraries (Lucene, Elasticsearch), Oracle Text indexes live inside the database alongside the data, enabling full-text search to participate in SQL joins, transactions, and access control with no external infrastructure.\n\nOracle Text is ideal for:\n- Document repositories and content management systems\n- Product catalog searches (fuzzy, stemming, thematic)\n- Regulatory document search (contracts, filings, correspondence)\n- Knowledge bases and FAQ systems\n- Any table with free-text VARCHAR2, CLOB, or XMLType columns\n\n---\n\n## Index Types\n\nOracle Text provides four primary index types. Choosing the right one is the most important decision.\n\n| Index Type | Best For | Notes |\n|---|---|---|\n| `CONTEXT` | Large documents, full-text CLOB/XMLType | Most powerful; batch or scheduled sync |\n| `CTXCAT` | Short text, catalog/e-commerce, ranked results | Supports complex query expressions; real-time |\n| `CTXRULE` | Routing/categorizing incoming documents | `MATCHES` operator; documents classified against query rules |\n| `CTXXPATH` | XPath queries on XMLType | Optimizes XMLType XPath predicates |\n\n---\n\n## CONTEXT Index: Full Document Search\n\n### Creating a CONTEXT Index\n\n```sql\n-- Minimal CONTEXT index (all defaults)\nCREATE INDEX idx_article_text\n ON articles (content)\n INDEXTYPE IS CTXSYS.CONTEXT;\n\n-- CONTEXT index with explicit preferences\nCREATE INDEX idx_product_desc\n ON products (description)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('\n LEXER my_lexer\n WORDLIST my_wordlist\n STOPLIST ctxsys.default_stoplist\n MEMORY 128M\n SYNC (ON COMMIT)\n ');\n\n-- Multi-column index: index multiple text columns as one\n-- First, create a user datastore that concatenates columns\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('product_store', 'MULTI_COLUMN_DATASTORE');\n CTX_DDL.SET_ATTRIBUTE('product_store', 'COLUMNS', 'title, description, tags');\nEND;\n/\n\nCREATE INDEX idx_product_fulltext\n ON products (title) -- first column; others specified in datastore\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('DATASTORE product_store SYNC (ON COMMIT)');\n```\n\n### Index Synchronization Modes\n\nCONTEXT indexes are not updated in real-time by default. New/modified rows must be synchronized into the index.\n\n```sql\n-- SYNC ON COMMIT: automatic sync after every commit (12c+; has overhead)\nPARAMETERS ('SYNC (ON COMMIT)')\n\n-- SYNC EVERY n seconds: background sync on a schedule\nPARAMETERS ('SYNC (EVERY \"SYSDATE + 1/24\")') -- sync every hour\n\n-- Manual sync (most common for batch systems)\nEXEC CTX_DDL.SYNC_INDEX('idx_article_text');\n-- With memory allocation\nEXEC CTX_DDL.SYNC_INDEX('idx_article_text', '128M');\n\n-- Optimize the index (merge fragmented doc lists, remove deleted doc entries)\nEXEC CTX_DDL.OPTIMIZE_INDEX('idx_article_text', 'FAST'); -- quick defrag\nEXEC CTX_DDL.OPTIMIZE_INDEX('idx_article_text', 'FULL'); -- full merge (slow but thorough)\nEXEC CTX_DDL.OPTIMIZE_INDEX('idx_article_text', 'TOKEN', maxtime => 300); -- 5 min max\n\n-- Check pending documents not yet indexed\nSELECT COUNT(*) FROM ctx_pending WHERE idx_name = 'IDX_ARTICLE_TEXT';\n```\n\n---\n\n## CTXCAT Index: Catalog Search\n\nCTXCAT is designed for catalog-style searches (short text, combo text+structured filters). Unlike CONTEXT, it updates automatically with DML (no manual sync).\n\n```sql\n-- Create a CTXCAT index (no sync needed)\nCREATE INDEX idx_product_cat\n ON products (product_name)\n INDEXTYPE IS CTXSYS.CTXCAT;\n\n-- CTXCAT with sub-indexes for structured attributes\nBEGIN\n CTX_DDL.CREATE_INDEX_SET('product_idx_set');\n CTX_DDL.ADD_INDEX('product_idx_set', 'price'); -- NUMBER\n CTX_DDL.ADD_INDEX('product_idx_set', 'category_id'); -- NUMBER\n CTX_DDL.ADD_INDEX('product_idx_set', 'brand'); -- VARCHAR2\nEND;\n/\n\nCREATE INDEX idx_product_ctxcat\n ON products (product_name)\n INDEXTYPE IS CTXSYS.CTXCAT\n PARAMETERS ('INDEX SET product_idx_set');\n```\n\n---\n\n## The CONTAINS Operator\n\n`CONTAINS` is the primary search operator for CONTEXT indexes. It returns a relevance score (0–100, where 100 is most relevant).\n\n```sql\n-- Basic keyword search (single word)\nSELECT product_id, product_name\nFROM products\nWHERE CONTAINS(description, 'widget') > 0;\n\n-- Multiple words (implicit AND)\nSELECT product_id, product_name\nFROM products\nWHERE CONTAINS(description, 'industrial widget') > 0;\n\n-- Relevance score in SELECT\nSELECT product_id, product_name,\n SCORE(1) AS relevance -- SCORE() must use same label as CONTAINS label\nFROM products\nWHERE CONTAINS(description, 'industrial widget', 1) > 0\nORDER BY relevance DESC;\n\n-- CATSEARCH for CTXCAT indexes\nSELECT product_id, product_name\nFROM products\nWHERE CATSEARCH(product_name, 'widget', 'category_id = 5 AND price \u003c 100') > 0;\n```\n\n---\n\n## Query Operators\n\nOracle Text supports a rich query language within the CONTAINS operator string.\n\n### Boolean Operators\n\n```sql\n-- AND: both terms must appear\nWHERE CONTAINS(text_col, 'oracle AND database') > 0;\n\n-- OR: either term\nWHERE CONTAINS(text_col, 'oracle OR mysql') > 0;\n\n-- NOT: exclude documents with term (NOT requires at least one positive term)\nWHERE CONTAINS(text_col, 'database NOT oracle') > 0;\n\n-- Shorthand: & = AND, | = OR, ~ = NOT\nWHERE CONTAINS(text_col, 'oracle & database ~ mysql') > 0;\n\n-- Precedence: NOT > AND > OR (use parentheses for clarity)\nWHERE CONTAINS(text_col, '(oracle | postgres) & (performance ~ slow)') > 0;\n```\n\n### Phrase Search\n\n```sql\n-- Exact phrase (words must appear adjacent in this order)\nWHERE CONTAINS(description, '{high performance widget}') > 0;\n\n-- Near: terms within N words of each other\nWHERE CONTAINS(description, 'oracle NEAR database') > 0;\nWHERE CONTAINS(description, 'oracle NEAR((database,performance), 5)') > 0;\n-- Within 5 words\n```\n\n### Fuzzy Search\n\nFuzzy search finds words that are similar (based on edit distance), useful for misspellings.\n\n```sql\n-- Fuzzy match for \"widget\" (finds \"wigdet\", \"widgit\", etc.)\nWHERE CONTAINS(description, 'fuzzy(widget)') > 0;\n\n-- Fuzzy with score threshold and expansion limit\nWHERE CONTAINS(description, 'fuzzy(widget, 60, 100, weight)') > 0;\n-- 60 = minimum similarity score, 100 = max expansions\n\n-- Combined fuzzy and exact\nWHERE CONTAINS(description, 'fuzzy(widgit) & premium') > 0;\n```\n\n### Stemming Search\n\nStemming finds morphological variants of a word (e.g., searching \"run\" also finds \"running\", \"ran\", \"runs\").\n\n```sql\n-- Stem operator: finds all morphological forms\nWHERE CONTAINS(description, 'stem(install)') > 0;\n-- Finds: install, installed, installing, installation, installs\n\nWHERE CONTAINS(description, 'stem(connect)') > 0;\n-- Finds: connect, connected, connecting, connection, connections\n\n-- Explicit: match only exact word (no stemming)\nWHERE CONTAINS(description, 'exact(install)') > 0;\n```\n\n### Wildcard Search\n\n```sql\n-- Right truncation (prefix search)\nWHERE CONTAINS(description, 'manag%') > 0;\n-- Finds: manage, manager, management, managing\n\n-- Left truncation (suffix search)\nWHERE CONTAINS(description, '%tion') > 0;\n-- Finds: action, connection, installation...\n\n-- Both sides\nWHERE CONTAINS(description, '%connect%') > 0;\n-- Finds: reconnect, disconnect, interconnection...\n```\n\n### Thematic Search\n\n```sql\n-- ABOUT: conceptual/thematic search (requires knowledge base)\nWHERE CONTAINS(description, 'about(database performance)') > 0;\n-- Finds documents conceptually related to \"database performance\"\n-- even if those exact words don't appear\n```\n\n---\n\n## Lexers and Wordlists: Language Configuration\n\n### Creating a Custom Lexer\n\n```sql\n-- Basic English lexer\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('my_english_lexer', 'BASIC_LEXER');\n CTX_DDL.SET_ATTRIBUTE('my_english_lexer', 'PRINTJOINS', '_-'); -- keep _ and - in tokens\n CTX_DDL.SET_ATTRIBUTE('my_english_lexer', 'MIXED_CASE', 'NO'); -- case-insensitive\n CTX_DDL.SET_ATTRIBUTE('my_english_lexer', 'BASE_LETTER', 'YES'); -- strip accents\nEND;\n/\n\n-- Multi-language lexer\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('global_lexer', 'WORLD_LEXER');\nEND;\n/\n```\n\n### Custom Wordlist for Fuzzy/Stemming\n\n```sql\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('my_wordlist', 'BASIC_WORDLIST');\n CTX_DDL.SET_ATTRIBUTE('my_wordlist', 'FUZZY_MATCH', 'ENGLISH');\n CTX_DDL.SET_ATTRIBUTE('my_wordlist', 'FUZZY_SCORE', '60');\n CTX_DDL.SET_ATTRIBUTE('my_wordlist', 'FUZZY_NUMRESULTS', '5000');\n CTX_DDL.SET_ATTRIBUTE('my_wordlist', 'STEMMER', 'ENGLISH');\n CTX_DDL.SET_ATTRIBUTE('my_wordlist', 'WILDCARD_MAXTERMS','5000');\nEND;\n/\n\n-- Apply in index creation\nCREATE INDEX idx_articles ON articles(content)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('LEXER my_english_lexer WORDLIST my_wordlist');\n```\n\n---\n\n## Multi-Column Indexes\n\nOracle Text can index multiple columns as a single searchable unit using datastores.\n\n```sql\n-- Concatenate multiple columns into one index\nBEGIN\n CTX_DDL.DROP_PREFERENCE('product_multistore');\n CTX_DDL.CREATE_PREFERENCE('product_multistore', 'MULTI_COLUMN_DATASTORE');\n CTX_DDL.SET_ATTRIBUTE('product_multistore', 'COLUMNS',\n 'product_name, short_description, long_description, keywords, brand_name');\n CTX_DDL.SET_ATTRIBUTE('product_multistore', 'DELIMITER', 'NEWLINE');\nEND;\n/\n\nCREATE INDEX idx_product_search\n ON products (product_name) -- anchor column (must exist on table)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('DATASTORE product_multistore SYNC (ON COMMIT)');\n\n-- Search finds matches in ANY of the indexed columns\nSELECT product_id, product_name\nFROM products\nWHERE CONTAINS(product_name, 'industrial grade widget') > 0;\n```\n\n### URL/File Datastore\n\n```sql\n-- Index content from files on the OS (FILE_DATASTORE)\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('file_store', 'FILE_DATASTORE');\n CTX_DDL.SET_ATTRIBUTE('file_store', 'PATH', '/data/documents');\nEND;\n/\n\nCREATE TABLE document_index (\n doc_id NUMBER PRIMARY KEY,\n filename VARCHAR2(500) -- column contains file paths\n);\n\nCREATE INDEX idx_documents\n ON document_index(filename)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('DATASTORE file_store');\n\n-- Index content from URLs (URL_DATASTORE)\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('url_store', 'URL_DATASTORE');\n CTX_DDL.SET_ATTRIBUTE('url_store', 'TIMEOUT', '30');\n CTX_DDL.SET_ATTRIBUTE('url_store', 'HTTP_PROXY', 'proxy.mycompany.com');\nEND;\n/\n```\n\n---\n\n## HIGHLIGHT and SNIPPET Functions\n\nThese functions generate context-aware result display, similar to Google's excerpt highlighting.\n\n### CTX_DOC.HIGHLIGHT\n\n```sql\n-- Highlight matching terms in a stored document\nDECLARE\n v_markup CLOB;\nBEGIN\n CTX_DOC.MARKUP(\n index_name => 'IDX_ARTICLE_TEXT',\n textkey => '42', -- primary key of the document\n text_query => 'database performance',\n restab => v_markup,\n starttag => '\u003cb>', -- opening highlight tag\n endtag => '\u003c/b>' -- closing highlight tag\n );\n DBMS_OUTPUT.PUT_LINE(DBMS_LOB.SUBSTR(v_markup, 4000, 1));\nEND;\n```\n\n### CTX_DOC.SNIPPET (Most Useful for Search UIs)\n\n`SNIPPET` extracts the most relevant sections of a document (the \"hits in context\") as short excerpts with highlighted terms.\n\n```sql\nDECLARE\n v_snippet VARCHAR2(4000);\nBEGIN\n CTX_DOC.SNIPPET(\n index_name => 'IDX_ARTICLE_TEXT',\n textkey => TO_CHAR(42), -- must be VARCHAR2\n text_query => 'database performance',\n restab => v_snippet,\n starttag => '\u003cem>',\n endtag => '\u003c/em>',\n separator => '...', -- between excerpts\n numsnippets=> 3, -- number of excerpt fragments\n snippetlen => 200 -- characters per snippet\n );\n DBMS_OUTPUT.PUT_LINE(v_snippet);\nEND;\n```\n\n### Using CTX_DOC in SQL Queries\n\n```sql\n-- Generate snippets for search results (inline)\nSELECT a.article_id,\n a.title,\n SCORE(1) AS relevance,\n CTX_DOC.SNIPPET_QUERY('IDX_ARTICLE_TEXT',\n ROWID,\n 'database performance',\n starttag => '\u003cb>',\n endtag => '\u003c/b>',\n numsnippets => 2) AS excerpt\nFROM articles a\nWHERE CONTAINS(a.content, 'database performance', 1) > 0\nORDER BY relevance DESC\nFETCH FIRST 10 ROWS ONLY;\n```\n\n---\n\n## Index Maintenance: Sync and Optimize\n\n### Sync: Index New/Updated Documents\n\n```sql\n-- Manual sync: index pending changes\nEXEC CTX_DDL.SYNC_INDEX('IDX_ARTICLE_TEXT');\n\n-- With memory tuning (larger = faster for big batches)\nEXEC CTX_DDL.SYNC_INDEX('IDX_ARTICLE_TEXT', '256M');\n\n-- Check pending documents\nSELECT COUNT(*) FROM ctx_pending WHERE idx_name = 'IDX_ARTICLE_TEXT';\n\n-- Schedule sync with DBMS_SCHEDULER\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'SYNC_ARTICLE_INDEX',\n job_type => 'PLSQL_BLOCK',\n job_action => 'CTX_DDL.SYNC_INDEX(''IDX_ARTICLE_TEXT'', ''64M'');',\n start_date => SYSTIMESTAMP,\n repeat_interval => 'FREQ=MINUTELY; INTERVAL=15', -- every 15 minutes\n enabled => TRUE\n );\nEND;\n```\n\n### Optimize: Defragment the Index\n\nOver time, as documents are updated and deleted, the CONTEXT index becomes fragmented. Optimization merges fragmented posting lists.\n\n```sql\n-- Fast optimization: quick cleanup pass\nEXEC CTX_DDL.OPTIMIZE_INDEX('IDX_ARTICLE_TEXT', 'FAST');\n\n-- Full optimization: complete merge (can take hours on large indexes)\nEXEC CTX_DDL.OPTIMIZE_INDEX('IDX_ARTICLE_TEXT', 'FULL');\n\n-- Token-based optimization: spend at most N seconds\nEXEC CTX_DDL.OPTIMIZE_INDEX('IDX_ARTICLE_TEXT', 'TOKEN', maxtime => 1800); -- 30 min\n\n-- Schedule nightly optimization\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'OPTIMIZE_ARTICLE_INDEX',\n job_type => 'PLSQL_BLOCK',\n job_action => 'CTX_DDL.OPTIMIZE_INDEX(''IDX_ARTICLE_TEXT'', ''FAST'');',\n start_date => TRUNC(SYSTIMESTAMP) + 1 + 2/24, -- 2 AM next day\n repeat_interval => 'FREQ=DAILY; BYHOUR=2; BYMINUTE=0',\n enabled => TRUE\n );\nEND;\n```\n\n---\n\n## Monitoring Oracle Text Indexes\n\n```sql\n-- Index status and statistics\nSELECT idx_name, idx_type, idx_status, idx_language,\n idx_option, idx_docid_count, idx_sync_interval\nFROM ctx_indexes;\n\n-- Index errors during sync (important for troubleshooting)\nSELECT * FROM ctx_index_errors\nWHERE err_index_name = 'IDX_ARTICLE_TEXT'\nORDER BY err_timestamp DESC;\n\n-- Token statistics (useful for analyzing query performance)\nSELECT token_text, token_count, token_doc_count\nFROM dr$idx_article_text$i -- index table: dr$\u003cindex_name>$i\nWHERE token_text = 'database'\nORDER BY token_doc_count DESC;\n\n-- Pending rows to be synced\nSELECT * FROM ctx_pending\nWHERE idx_name = 'IDX_ARTICLE_TEXT';\n\n-- User-defined preferences\nSELECT pre_name, pre_class, pre_object, pre_attribute, pre_value\nFROM ctx_user_preferences;\n```\n\n---\n\n## Section Groups: Structured Text Search\n\nSection groups allow you to search within specific parts of HTML or XML documents.\n\n```sql\n-- HTML section group\nBEGIN\n CTX_DDL.CREATE_SECTION_GROUP('html_sections', 'HTML_SECTION_GROUP');\n CTX_DDL.ADD_ZONE_SECTION('html_sections', 'title', 'title'); -- HTML \u003ctitle>\n CTX_DDL.ADD_ZONE_SECTION('html_sections', 'heading','h1'); -- HTML \u003ch1>\n CTX_DDL.ADD_ZONE_SECTION('html_sections', 'para', 'p'); -- HTML \u003cp>\nEND;\n/\n\nCREATE INDEX idx_html_content ON web_pages(html_content)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('SECTION GROUP html_sections FORMAT HTML');\n\n-- Search within specific HTML sections\nSELECT page_id, url\nFROM web_pages\nWHERE CONTAINS(html_content, 'oracle WITHIN title') > 0;\n-- Only matches documents where \"oracle\" appears in a \u003ctitle> tag\n\n-- XML section group\nBEGIN\n CTX_DDL.CREATE_SECTION_GROUP('contract_sections', 'XML_SECTION_GROUP');\n CTX_DDL.ADD_ZONE_SECTION('contract_sections', 'terms', 'Terms');\n CTX_DDL.ADD_ZONE_SECTION('contract_sections', 'definitions','Definitions');\n CTX_DDL.ADD_ZONE_SECTION('contract_sections', 'liability', 'Liability');\nEND;\n/\n\n-- Find contracts mentioning \"damages\" specifically in the Liability section\nSELECT contract_id, contract_number\nFROM contracts\nWHERE CONTAINS(contract_xml_text, 'damages WITHIN liability') > 0;\n```\n\n---\n\n## Best Practices\n\n- **Choose `CTXCAT` for short text that updates frequently** (product names, titles, tags). It maintains itself automatically. Use `CONTEXT` for large documents.\n- **Schedule `SYNC_INDEX` based on acceptable staleness.** `SYNC ON COMMIT` has overhead; `SYNC EVERY n MINUTES` via the scheduler is usually better.\n- **Run `OPTIMIZE_INDEX` regularly** (weekly or nightly for active systems). Fragmented indexes return degraded relevance scores.\n- **Use `SCORE()` to rank results** and filter with `CONTAINS > threshold` rather than `> 0` to eliminate marginally relevant results.\n- **Index only columns that actually need full-text search.** Oracle Text indexes consume significant storage (often 20–40% of the original data size).\n- **Use section groups** for structured documents to enable section-scoped searches rather than whole-document searches.\n- **Test fuzzy and stem parameters** with your actual data before going to production. Overly aggressive fuzzy matching returns too many irrelevant results.\n- **Use `MULTI_COLUMN_DATASTORE`** instead of creating separate indexes on each column. One index on all text columns is faster to query than `CONTAINS(col1, q) > 0 OR CONTAINS(col2, q) > 0`.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Querying Immediately After DML (Before Sync)\n\n```sql\nINSERT INTO articles (article_id, content) VALUES (999, 'New article about Oracle performance');\nCOMMIT;\n\n-- WRONG: this may return 0 rows if index has not been synced\nSELECT * FROM articles WHERE CONTAINS(content, 'Oracle performance') > 0;\n\n-- RIGHT: ensure sync if real-time search is needed\nEXEC CTX_DDL.SYNC_INDEX('IDX_ARTICLES');\nSELECT * FROM articles WHERE CONTAINS(content, 'Oracle performance') > 0;\n```\n\n### Mistake 2: Using LIKE Instead of CONTAINS\n\n```sql\n-- WRONG for full-text: LIKE does a full table scan, ignores Text index\nWHERE description LIKE '%high performance widget%'\n\n-- RIGHT: use CONTAINS for indexed full-text search\nWHERE CONTAINS(description, '{high performance widget}') > 0\n```\n\n### Mistake 3: Forgetting to Optimize After Mass Deletes/Updates\n\nWhen you delete or update many documents and don't run `OPTIMIZE_INDEX`, the index accumulates stale \"garbage\" entries. This bloats the index and degrades query performance. After any bulk DML, run `CTX_DDL.OPTIMIZE_INDEX` with `FAST` or `FULL`.\n\n### Mistake 4: Wrong Filter for Binary Document Types\n\nIf you index a column that stores Word documents, PDFs, or HTML (as BLOBs), you must set the `FILTER` preference to `INSO_FILTER` or `AUTO_FILTER`. Without this, Oracle Text indexes raw binary content (garbage).\n\n```sql\nBEGIN\n CTX_DDL.CREATE_PREFERENCE('auto_filter', 'AUTO_FILTER');\nEND;\n/\n\nCREATE INDEX idx_docs ON documents(content_blob)\n INDEXTYPE IS CTXSYS.CONTEXT\n PARAMETERS ('FILTER auto_filter FORMAT COLUMN format_col');\n```\n\n### Mistake 5: Not Using SCORE() for Ranking\n\n```sql\n-- Missing relevance ordering\nSELECT * FROM articles WHERE CONTAINS(content, 'database', 1) > 0;\n-- Returns results in undefined order\n\n-- Always use SCORE() to rank by relevance\nSELECT article_id, title, SCORE(1) AS rel\nFROM articles\nWHERE CONTAINS(content, 'database', 1) > 0\nORDER BY rel DESC;\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Text Reference 19c (CCREF)](https://docs.oracle.com/en/database/oracle/oracle-database/19/ccref/)\n- [Oracle Text Application Developer's Guide 19c (CCAPP)](https://docs.oracle.com/en/database/oracle/oracle-database/19/ccapp/)\n- [Oracle Database 19c SQL Language Reference — CONTAINS](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20514,"content_sha256":"f017dcdb7df4f4eabfb6e4200067891eb87db9496ca9b878764fb38fbfe05934"},{"filename":"appdev/python-oracledb.md","content":"# Python + Oracle Database\n\n## Overview\n\n`python-oracledb` is the official Oracle Python driver (successor to `cx_Oracle`). It supports two modes:\n\n- **Thin mode** (default): pure Python, no Oracle Client libraries required. Supports most features.\n- **Thick mode**: requires Oracle Client (Instant Client or full client). Required for advanced features like Advanced Queuing, Sharding, and some proxy authentication scenarios.\n\n```bash\npip install oracledb\n```\n\n---\n\n## Connecting\n\n### Basic Connection (Thin Mode)\n\n```python\nimport oracledb\n\n# Easy Connect string\nconn = oracledb.connect(\n user=\"hr\",\n password=\"password\",\n dsn=\"localhost:1521/freepdb1\"\n)\n\n# TNS alias (requires tnsnames.ora in TNS_ADMIN path)\nconn = oracledb.connect(\n user=\"hr\",\n password=\"password\",\n dsn=\"mydb_high\"\n)\n\n# Close explicitly or use context manager\nwith oracledb.connect(user=\"hr\", password=\"password\", dsn=\"localhost:1521/freepdb1\") as conn:\n with conn.cursor() as cur:\n cur.execute(\"SELECT sysdate FROM dual\")\n print(cur.fetchone())\n```\n\n### Wallet / mTLS (Autonomous Database)\n\n```python\nimport oracledb\n\nconn = oracledb.connect(\n user=\"admin\",\n password=\"password\",\n dsn=\"myatp_high\", # TNS alias from tnsnames.ora in wallet\n config_dir=\"/path/to/wallet\",\n wallet_location=\"/path/to/wallet\",\n wallet_password=\"wallet_password\" # if the wallet is password-protected\n)\n```\n\n### Enabling Thick Mode\n\n```python\nimport oracledb\n\n# Call before any connection — sets the mode for the entire process\noracledb.init_oracle_client(lib_dir=\"/opt/oracle/instantclient_21_9\")\n\nconn = oracledb.connect(user=\"hr\", password=\"password\", dsn=\"localhost:1521/freepdb1\")\n```\n\n---\n\n## Executing SQL\n\n### Bind Variables\n\nAlways use bind variables — never format user input into SQL strings.\n\n```python\nwith conn.cursor() as cur:\n # Named binds (recommended)\n cur.execute(\n \"SELECT last_name, salary FROM employees WHERE department_id = :dept_id AND salary > :min_sal\",\n dept_id=60,\n min_sal=5000\n )\n\n # Or pass as dict\n cur.execute(\n \"SELECT last_name FROM employees WHERE employee_id = :id\",\n {\"id\": 100}\n )\n\n rows = cur.fetchall()\n for row in rows:\n print(row)\n```\n\n### DML with Binds\n\n```python\nwith conn.cursor() as cur:\n cur.execute(\n \"UPDATE employees SET salary = :sal WHERE employee_id = :id\",\n sal=9000,\n id=100\n )\n conn.commit()\n```\n\n### Batch Execution (executemany)\n\n```python\ndata = [\n {\"id\": 201, \"name\": \"Alice\", \"dept\": 10},\n {\"id\": 202, \"name\": \"Bob\", \"dept\": 20},\n {\"id\": 203, \"name\": \"Carol\", \"dept\": 10},\n]\n\nwith conn.cursor() as cur:\n cur.executemany(\n \"INSERT INTO employees (employee_id, last_name, department_id) VALUES (:id, :name, :dept)\",\n data\n )\n conn.commit()\n```\n\n---\n\n## Fetching Results\n\n```python\nwith conn.cursor() as cur:\n cur.execute(\"SELECT employee_id, last_name, salary FROM employees WHERE rownum \u003c= 100\")\n\n # fetchone — single row\n row = cur.fetchone()\n\n # fetchmany — batch\n rows = cur.fetchmany(numRows=25)\n\n # fetchall — all remaining (careful with large result sets)\n rows = cur.fetchall()\n\n # Iterate directly (memory-efficient for large sets)\n cur.execute(\"SELECT * FROM employees\")\n for row in cur:\n print(row)\n```\n\n### Column Names\n\n```python\nwith conn.cursor() as cur:\n cur.execute(\"SELECT employee_id, last_name, salary FROM employees WHERE rownum \u003c= 5\")\n columns = [col[0] for col in cur.description]\n rows = cur.fetchall()\n for row in rows:\n print(dict(zip(columns, row)))\n```\n\n### Fetch as Dictionaries (rowfactory)\n\n```python\ndef make_dict_factory(cursor):\n cols = [col[0].lower() for col in cursor.description]\n def create_row(*args):\n return dict(zip(cols, args))\n return create_row\n\nwith conn.cursor() as cur:\n cur.execute(\"SELECT employee_id, last_name FROM employees WHERE rownum \u003c= 5\")\n cur.rowfactory = make_dict_factory(cur)\n for row in cur:\n print(row) # {'employee_id': 100, 'last_name': 'King'}\n```\n\n---\n\n## Connection Pooling\n\nUse a connection pool for web applications and multi-threaded code. Never share a single connection across threads.\n\n```python\nimport oracledb\n\n# Create pool at application startup\npool = oracledb.create_pool(\n user=\"hr\",\n password=\"password\",\n dsn=\"localhost:1521/freepdb1\",\n min=2, # minimum open connections\n max=10, # maximum connections\n increment=1 # connections opened when more are needed\n)\n\n# Acquire a connection from the pool\nwith pool.acquire() as conn:\n with conn.cursor() as cur:\n cur.execute(\"SELECT COUNT(*) FROM employees\")\n print(cur.fetchone())\n\n# Pool is returned automatically; close pool at app shutdown\npool.close()\n```\n\n---\n\n## PL/SQL Calls\n\n```python\nimport oracledb\n\nwith conn.cursor() as cur:\n # Call a stored procedure\n cur.callproc(\"hr.update_salary\", [100, 9500])\n conn.commit()\n\n # Call a function\n result = cur.callfunc(\"hr.get_employee_count\", oracledb.DB_TYPE_NUMBER, [10])\n print(result)\n\n # Anonymous PL/SQL block with OUT parameters\n out_val = cur.var(oracledb.DB_TYPE_VARCHAR)\n cur.execute(\n \"\"\"\n BEGIN\n :out := 'Hello from PL/SQL';\n END;\n \"\"\",\n out=out_val\n )\n print(out_val.getvalue())\n```\n\n### REF CURSOR\n\n```python\nwith conn.cursor() as cur:\n ref_cursor = cur.var(oracledb.DB_TYPE_CURSOR)\n cur.execute(\n \"\"\"\n BEGIN\n OPEN :rc FOR SELECT employee_id, last_name FROM employees WHERE department_id = :dept;\n END;\n \"\"\",\n rc=ref_cursor,\n dept=60\n )\n for row in ref_cursor.getvalue():\n print(row)\n```\n\n---\n\n## LOB Handling\n\n```python\nimport oracledb\n\n# Read a CLOB\nwith conn.cursor() as cur:\n cur.execute(\"SELECT resume FROM employee_docs WHERE employee_id = 100\")\n row = cur.fetchone()\n if row:\n clob = row[0]\n text = clob.read() # reads entire CLOB as string\n print(text[:200])\n\n# Write a CLOB\nwith conn.cursor() as cur:\n large_text = \"...\" * 10000\n cur.execute(\n \"UPDATE employee_docs SET resume = :clob WHERE employee_id = :id\",\n clob=large_text,\n id=100\n )\n conn.commit()\n\n# Read a BLOB\nwith conn.cursor() as cur:\n cur.execute(\"SELECT photo FROM employee_photos WHERE employee_id = 100\")\n row = cur.fetchone()\n if row:\n blob_data = row[0].read()\n with open(\"photo.jpg\", \"wb\") as f:\n f.write(blob_data)\n```\n\n---\n\n## Async Support (python-oracledb 2.x)\n\n```python\nimport asyncio\nimport oracledb\n\nasync def main():\n conn = await oracledb.connect_async(\n user=\"hr\",\n password=\"password\",\n dsn=\"localhost:1521/freepdb1\"\n )\n async with conn.cursor() as cur:\n await cur.execute(\"SELECT COUNT(*) FROM employees\")\n row = await cur.fetchone()\n print(row)\n await conn.close()\n\nasyncio.run(main())\n\n# Async pool\nasync def pooled():\n pool = oracledb.create_pool_async(\n user=\"hr\", password=\"password\", dsn=\"localhost:1521/freepdb1\",\n min=2, max=10, increment=1\n )\n async with pool.acquire() as conn:\n async with conn.cursor() as cur:\n await cur.execute(\"SELECT 1 FROM dual\")\n print(await cur.fetchone())\n await pool.close()\n\nasyncio.run(pooled())\n```\n\n---\n\n## Best Practices\n\n- **Always use bind variables** — never concatenate user input into SQL.\n- **Use a connection pool** in web/multi-threaded apps; do not share connections across threads.\n- **Use context managers** (`with`) for connections and cursors to ensure cleanup.\n- **Use `executemany`** for bulk inserts/updates instead of looping `execute`.\n- **Set `arraysize`** on the cursor for large fetches: `cur.arraysize = 1000` reduces round-trips.\n- **Commit explicitly** — `python-oracledb` does not auto-commit.\n- **Prefer thin mode** unless you specifically need thick-mode-only features.\n\n---\n\n## Common Mistakes\n\n| Mistake | Problem | Fix |\n|---------|---------|-----|\n| `f\"SELECT ... WHERE id = {user_input}\"` | SQL injection | Use bind variables |\n| Sharing one connection across threads | Corruption, errors | Use a pool; one connection per thread |\n| Not calling `conn.commit()` | DML silently rolled back on disconnect | Always commit or use `autocommit=True` for simple scripts |\n| `fetchall()` on a million-row query | OOM | Use `fetchmany()` or iterate the cursor |\n| Using `cx_Oracle` in new projects | Deprecated | Migrate to `python-oracledb` |\n| Not setting `arraysize` for large fetches | Excessive round-trips | `cur.arraysize = 1000` before fetch |\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Thin mode is supported for Oracle Database 12.1 and later.\n- `python-oracledb` 2.x adds async support and JSON improvements for 21c+.\n- Oracle 23ai JSON Relational Duality Views are accessible via normal SELECT queries.\n\n## Sources\n\n- [python-oracledb Documentation](https://python-oracledb.readthedocs.io/)\n- [python-oracledb GitHub](https://github.com/oracle/python-oracledb)\n- [Oracle Python Developer Center](https://www.oracle.com/database/technologies/appdev/python.html)\n- [Migrating from cx_Oracle to python-oracledb](https://python-oracledb.readthedocs.io/en/latest/user_guide/appendix_c.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9368,"content_sha256":"8fa9008b87a219000f7e3e13b819c4b2ba5afb986d547cd992a091dd445e50e2"},{"filename":"appdev/sequences-identity.md","content":"# Sequences and Identity Columns in Oracle Database\n\n## Overview\n\nGenerating unique, monotonically increasing identifiers is a fundamental requirement in relational database design. Oracle provides two mechanisms:\n\n- **Sequences** — standalone schema objects that generate numbers independently of any table\n- **Identity Columns** — column-level syntax that combines sequence generation with table DDL (introduced in Oracle 12c)\n\nBoth ultimately use the same internal sequence engine, but identity columns provide a cleaner, more declarative way to define surrogate keys.\n\n---\n\n## CREATE SEQUENCE: Full Syntax and Options\n\n```sql\nCREATE SEQUENCE schema.sequence_name\n START WITH initial_value -- first value generated (default: 1)\n INCREMENT BY step_value -- positive = ascending, negative = descending (default: 1)\n MINVALUE min_value -- lower bound\n MAXVALUE max_value -- upper bound (default: 10^27 for ASC)\n NOCYCLE | CYCLE -- NOCYCLE raises ORA-08004 at limit (default: NOCYCLE)\n CACHE n | NOCACHE -- Use memory for a sequence increment count to improve performance (default: 20)\n ORDER | NOORDER -- ORDER guarantees generation order (only matters for RAC)\n KEEP | NOKEEP -- affects sequence behavior after session restore\n SCALE | NOSCALE; -- introduced in 23ai: prepend instance/shard info for uniqueness\n```\n\n### Basic Examples\n\n```sql\n-- Simple primary key sequence (most common pattern)\nCREATE SEQUENCE seq_customer_id\n START WITH 1000\n INCREMENT BY 1\n MAXVALUE 9999999999\n NOCYCLE\n CACHE 100;\n\n-- Descending sequence\nCREATE SEQUENCE seq_priority_desc\n START WITH 10000\n INCREMENT BY -1\n MINVALUE 1\n NOCYCLE\n CACHE 50;\n\n-- Step sequence for generating even numbers\nCREATE SEQUENCE seq_even_numbers\n START WITH 2\n INCREMENT BY 2\n MAXVALUE 1000000\n NOCYCLE\n CACHE 20;\n```\n\n### Using a Sequence\n\n```sql\n-- NEXTVAL: advances the sequence and returns the new value\n-- CURRVAL: returns the value last generated by NEXTVAL in this session (no advance)\n\n-- In an INSERT\nINSERT INTO customers (customer_id, name, email)\nVALUES (seq_customer_id.NEXTVAL, 'Acme Corp', '[email protected]');\n\n-- Capture the generated value in PL/SQL\nDECLARE\n v_new_id customers.customer_id%TYPE;\nBEGIN\n INSERT INTO customers (customer_id, name, email)\n VALUES (seq_customer_id.NEXTVAL, 'Beta LLC', '[email protected]')\n RETURNING customer_id INTO v_new_id;\n\n -- Use v_new_id for child record inserts\n INSERT INTO customer_contacts (contact_id, customer_id, contact_name)\n VALUES (seq_contacts.NEXTVAL, v_new_id, 'John Smith');\n\n COMMIT;\nEND;\n\n-- In a SELECT (useful for generating row numbers without a table)\nSELECT seq_customer_id.NEXTVAL FROM DUAL;\n\n-- CURRVAL in the same session after NEXTVAL has been called\nSELECT seq_customer_id.CURRVAL FROM DUAL;\n```\n\n### Altering and Dropping Sequences\n\n```sql\n-- Change cache size\nALTER SEQUENCE seq_customer_id CACHE 500;\n\n-- Restart at a new value (useful after data migration)\nALTER SEQUENCE seq_customer_id RESTART START WITH 50000; -- 18c+\n\n-- Pre-18c: approximate restart by setting INCREMENT to a large value\nSELECT last_number FROM user_sequences WHERE sequence_name = 'SEQ_CUSTOMER_ID';\n-- If current is 1000 and you want to restart at 50000, set increment to 49000\nALTER SEQUENCE seq_customer_id INCREMENT BY 49000;\nSELECT seq_customer_id.NEXTVAL FROM DUAL; -- now at 50000\nALTER SEQUENCE seq_customer_id INCREMENT BY 1;\n\n-- Drop a sequence\nDROP SEQUENCE seq_customer_id;\n```\n\n---\n\n## The CACHE Option: Performance Impact\n\n`CACHE` is the most impactful sequence parameter for performance. Oracle uses the SGA to increment `CACHE` values before synchronising with the data dictionary table (SEQ$). Incrementing a cached sequence is a pure in-memory operation — extremely fast. Without caching (`NOCACHE`), every `NEXTVAL` requires a write to the `seq db — Skillopedia system table.\n\n### Performance Comparison\n\nIn 19c+ as long as you do not specify NOCACHE, a cached sequence will have its cache size automatically tuned by the database to achieve optimal performance. If you want to explicitly set a CACHE, these are some guidelines\n\n\n| Configuration | NEXTVAL Cost | Notes |\n|---|---|---|\n| `CACHE 20` (default) | In-memory increment | Good for low-volume |\n| `CACHE 100` | In-memory increment | Good for moderate OLTP |\n| `CACHE 1000+` | In-memory increment | Bulk inserts, high-volume |\n\n```sql\n-- Check sequence cache performance in AWR\nSELECT object_name, value AS total_waits\nFROM v$sesstat st\nJOIN v$statname sn ON st.statistic# = sn.statistic#\nWHERE sn.name = 'sequence misses'\n AND st.sid = SYS_CONTEXT('USERENV', 'SID');\n```\n\n### Cache Loss on Instance Restart\n\nWhen the database restarts, cached but unused sequence values are **discarded**. If you cached 1000 values and used 50, the next session starts at value 1051 — a gap of 950. This is normal and expected. If your application cannot tolerate gaps, you cannot use sequences (and will pay the performance penalty).\n\n```sql\n-- How many values will be lost on restart?\nSELECT sequence_name, last_number, cache_size,\n last_number + cache_size - 1 AS max_cached_value,\n (cache_size - 1) AS max_possible_gap\nFROM user_sequences\nWHERE sequence_name = 'SEQ_CUSTOMER_ID';\n```\n\n---\n\n## Gaps in Sequences\n\nSequence values are **not gap-free** by design. Gaps occur because:\n\n1. **Cache loss on restart** — unused cached values are discarded\n2. **Rollback** — `NEXTVAL` is never rolled back; a rolled-back INSERT wastes the value\n3. **CYCLE** — when the sequence wraps around\n4. **DELETE** — rows (and their sequence values) can be deleted\n\n```sql\n-- Sequence values are NOT rolled back\nBEGIN\n INSERT INTO orders (order_id) VALUES (seq_orders.NEXTVAL); -- uses 1001\n ROLLBACK; -- INSERT undone, but 1001 is gone\nEND;\n\nBEGIN\n INSERT INTO orders (order_id) VALUES (seq_orders.NEXTVAL); -- uses 1002\n COMMIT;\nEND;\n-- Gap: 1001 is missing from orders table\n```\n\n**If gap-free numbering is required:** Use a different mechanism, such as a dedicated table with a row lock or `DBMS_LOCK` for serialization. This is almost always a mistake — gap-free sequences require serial execution and are a performance bottleneck. Business requirements for \"no gaps\" in numbering (e.g., invoice numbers for tax compliance) should use a separate post-commit numbering step.\n\n---\n\n## ORDER vs. NOORDER (RAC Environments)\n\nIn Oracle RAC (Real Application Clusters), multiple instances can generate sequence values independently from their own cache. With `NOORDER` (default), values are generated in no guaranteed order across nodes — a node 1 might generate 100–199, node 2 generates 200–299, and requests could get 150, 250, 160, 210 in interleaved order.\n\nWith `ORDER`, Oracle ensures strictly ordered generation across all RAC nodes by serializing through the cluster interconnect. This has significant performance implications.\n\n```sql\n-- ORDER sequence (only needed for RAC when strict ordering matters)\nCREATE SEQUENCE seq_ordered_id\n ORDER\n CACHE 10; -- smaller cache beneficial with ORDER; large cache wastes values\n\n-- For most applications on RAC, NOORDER + CACHE is the right choice\nCREATE SEQUENCE seq_fast_id\n NOORDER\n CACHE 500;\n```\n\n---\n\n## Identity Columns (12c+)\n\nIdentity columns are the modern way to define auto-incrementing primary keys. Oracle automatically creates and manages an underlying sequence.\n\n### GENERATED ALWAYS\n\nThe column value is **always** generated by Oracle. You cannot insert or update the column directly.\n\n```sql\nCREATE TABLE employees (\n employee_id NUMBER GENERATED ALWAYS AS IDENTITY\n (START WITH 1000 INCREMENT BY 1 CACHE 100),\n first_name VARCHAR2(50) NOT NULL,\n last_name VARCHAR2(50) NOT NULL,\n hire_date DATE DEFAULT SYSDATE NOT NULL,\n CONSTRAINT pk_employees PRIMARY KEY (employee_id)\n);\n\n-- Correct INSERT: omit the identity column\nINSERT INTO employees (first_name, last_name) VALUES ('Jane', 'Doe');\n\n-- Wrong: explicit value rejected\nINSERT INTO employees (employee_id, first_name, last_name)\nVALUES (9999, 'Jane', 'Doe');\n-- ORA-32795: cannot insert into a generated always identity column\n```\n\n### GENERATED BY DEFAULT\n\nOracle generates the value if you omit the column. You can override with an explicit value.\n\n```sql\nCREATE TABLE products (\n product_id NUMBER GENERATED BY DEFAULT AS IDENTITY,\n product_name VARCHAR2(100) NOT NULL,\n sku VARCHAR2(50) NOT NULL UNIQUE\n);\n\n-- Oracle generates the ID\nINSERT INTO products (product_name, sku) VALUES ('Widget Pro', 'WGT-001');\n\n-- Override with explicit value (useful for data migration)\nINSERT INTO products (product_id, product_name, sku) VALUES (9999, 'Legacy Widget', 'WGT-LEG');\n```\n\n### GENERATED BY DEFAULT ON NULL\n\nLike `GENERATED BY DEFAULT`, but also generates a value when `NULL` is explicitly inserted:\n\n```sql\nCREATE TABLE sessions (\n session_id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY,\n session_token VARCHAR2(64) NOT NULL,\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP\n);\n\n-- NULL triggers generation\nINSERT INTO sessions (session_id, session_token) VALUES (NULL, 'abc123');\n-- Oracle generates session_id instead of inserting NULL\n```\n\n### Retrieving Generated Identity Values\n\n```sql\n-- In PL/SQL using RETURNING\nDECLARE\n v_new_id employees.employee_id%TYPE;\nBEGIN\n INSERT INTO employees (first_name, last_name)\n VALUES ('Bob', 'Smith')\n RETURNING employee_id INTO v_new_id;\n\n DBMS_OUTPUT.PUT_LINE('New employee ID: ' || v_new_id);\n COMMIT;\nEND;\n```\n\n```java\n// In JDBC\nString sql = \"INSERT INTO employees (first_name, last_name) VALUES (?, ?)\";\ntry (PreparedStatement ps = conn.prepareStatement(sql, new String[]{\"EMPLOYEE_ID\"})) {\n ps.setString(1, \"Bob\");\n ps.setString(2, \"Smith\");\n ps.executeUpdate();\n\n try (ResultSet generatedKeys = ps.getGeneratedKeys()) {\n if (generatedKeys.next()) {\n long newId = generatedKeys.getLong(1);\n System.out.println(\"Generated ID: \" + newId);\n }\n }\n}\n```\n\n### Viewing the Underlying Sequence\n\n```sql\n-- Identity columns have an underlying sequence managed by Oracle\nSELECT column_name, identity_column, data_default,\n generation_type, sequence_name, increment_by, start_with, cache_size\nFROM user_tab_identity_cols\nWHERE table_name = 'EMPLOYEES';\n```\n\n---\n\n## SYS_GUID() vs. Sequences for UUIDs\n\nOracle provides `SYS_GUID()` (System Globally Unique Identifier) as an alternative to sequences for generating unique identifiers. SYS_GUID returns a 16-byte RAW value (displayed as 32 hex characters).\n\n### Comparison\n\n| Feature | Sequence | SYS_GUID() |\n|---|---|---|\n| Data type | NUMBER | RAW(16) |\n| Value size | ~10 bytes | 16 bytes |\n| Index efficiency | Excellent (monotonic) | Poor (random, causes index fragmentation) |\n| Globally unique | Only per DB | Yes |\n| Human readable | Yes | No (hex string) |\n| Performance | Excellent (cached) | Good (but random) |\n| Cross-database merge | Conflicts possible | Unique |\n\n```sql\n-- SYS_GUID() usage\nCREATE TABLE distributed_events (\n event_id RAW(16) DEFAULT SYS_GUID() PRIMARY KEY,\n event_type VARCHAR2(50),\n payload CLOB,\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP\n);\n\nINSERT INTO distributed_events (event_type, payload)\nVALUES ('ORDER_PLACED', '{\"order_id\": 1001}');\n\n-- Display as readable UUID\nSELECT RAWTOHEX(event_id) AS event_id_hex,\n REGEXP_REPLACE(RAWTOHEX(event_id),\n '([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})',\n '\\1-\\2-\\3-\\4-\\5') AS uuid_formatted,\n event_type\nFROM distributed_events;\n```\n\n### Standard UUID (VARCHAR2) Approach\n\nIf you need RFC 4122 UUID strings (e.g., for REST APIs that expose IDs):\n\n```sql\n-- Store as VARCHAR2 with a check constraint\nCREATE TABLE api_resources (\n resource_id VARCHAR2(36) DEFAULT LOWER(\n REGEXP_REPLACE(RAWTOHEX(SYS_GUID()),\n '([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})',\n '\\1-\\2-\\3-\\4-\\5')) PRIMARY KEY,\n resource_type VARCHAR2(50),\n data CLOB\n);\n\n-- Or generate in application and pass in\nINSERT INTO api_resources (resource_id, resource_type, data)\nVALUES (LOWER(REGEXP_REPLACE(RAWTOHEX(SYS_GUID()),\n '([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})',\n '\\1-\\2-\\3-\\4-\\5')),\n 'ORDER', '{}');\n```\n\n### When to Use Each\n\n- **Sequences / Identity columns**: Default choice for primary keys in any application. Fast, compact, index-friendly.\n- **SYS_GUID() / UUID**: Use when records will be merged across multiple databases, when IDs are exposed in APIs (avoids enumeration attacks since values are non-sequential), or when generating IDs outside the database before inserting.\n\n**Important**: Random UUIDs as primary keys cause **index fragmentation**. Each new UUID inserts into a random position in the B-tree index, causing frequent page splits. For high-insert tables, this can degrade performance significantly compared to monotonic sequence values. Consider using a sequence for the database key and a UUID for the external-facing API key.\n\n---\n\n## Best Practices\n\n- **Use `CACHE 100` or higher** for OLTP sequences. The default of 20 is too low for high-insert systems.\n- **Prefer identity columns over manual sequence-in-insert** for new tables. The syntax is cleaner and the intent is clearer.\n- **Never assume gap-free sequences**. Design your application to tolerate gaps. If business rules require consecutive numbering (invoices, receipts), generate that number in a separate post-commit step.\n- **Use `GENERATED ALWAYS`** unless you have a specific reason to override. It prevents accidental manual inserts.\n- **For bulk data loads**, set `CACHE` very high (e.g., `CACHE 10000`) before the load and return it to normal afterward to minimize redo contention.\n- **In RAC environments**, use `NOORDER` with sufficient CACHE. The performance cost of `ORDER` is rarely worth it.\n- **Avoid `NOCYCLE`** unless you have audited the maximum range. Silent wraparound (with `CYCLE`) will corrupt primary key uniqueness.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Using NOCACHE for \"Auditing\" Purposes\n\nSome teams use `NOCACHE` because they want \"no gaps\" for audit sequences. This is the worst-of-both-worlds approach: it's still not truly gap-free (gaps occur on rollback), and it's vastly slower because every `NEXTVAL` writes to the data dictionary.\n\n### Mistake 2: Using Sequences with CYCLE Without MAXVALUE\n\n```sql\n-- DANGEROUS: default MAXVALUE is 10^27; with CYCLE, it eventually wraps\n-- to 1, colliding with existing primary key values\nCREATE SEQUENCE seq_bad CYCLE; -- omitted MAXVALUE\n\n-- SAFE: set MAXVALUE to something realistic and handle CYCLE explicitly\n-- Or just use NOCYCLE (default) and monitor for exhaustion\n```\n\n### Mistake 3: Selecting NEXTVAL in Bulk Without LIMIT\n\n```sql\n-- This generates 1 million sequence values instantly — probably not intended\nSELECT seq_orders.NEXTVAL FROM some_large_table;\n```\n\n### Mistake 4: Relying on CURRVAL Across Sessions\n\n`CURRVAL` returns the last value generated by `NEXTVAL` in **the current session**. It has no meaning in another session or after reconnecting.\n\n### Mistake 5: Forgetting Identity Columns in Data Exports/Imports\n\nWhen using `GENERATED ALWAYS`, Data Pump exports include the sequence definition. During import, if the target table already has rows, the sequence may generate duplicate values. Always check `LAST_NUMBER` after import and reset the sequence.\n\n```sql\n-- After Data Pump import, fix the identity sequence\nSELECT max(employee_id) FROM employees; -- say it's 5432\nALTER TABLE employees MODIFY employee_id GENERATED ALWAYS AS IDENTITY (START WITH LIMIT VALUE);\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c SQL Language Reference — CREATE SEQUENCE](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html)\n- [Oracle Database 19c SQL Language Reference — Identity Columns](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html)\n- [Oracle Database 19c Application Developer's Guide (ADFNS)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16951,"content_sha256":"ccd342add13edac2ec0a4f9b61c41e2bf46562c21a8d5144fc76eb47617364e7"},{"filename":"appdev/spatial-data.md","content":"# Spatial Data in Oracle Database\n\n## Overview\n\nOracle Spatial and Graph (formerly Oracle Spatial) provides a native spatial data type, geometry operators, and spatial indexing within the Oracle Database. It is fully integrated with SQL, enabling spatial queries to participate in joins, aggregations, and query optimizations alongside standard relational data.\n\nOracle Spatial is built on the `MDSYS` schema and requires the Oracle Spatial option (included in Enterprise Edition; also available in Standard Edition 2 in recent versions as a subset). Oracle's spatial capabilities conform to the **OGC (Open Geospatial Consortium) Simple Features for SQL** standard.\n\n---\n\n## The SDO_GEOMETRY Type\n\nAll spatial data in Oracle is stored using the `SDO_GEOMETRY` object type, defined in the `MDSYS` schema.\n\n### SDO_GEOMETRY Structure\n\n```sql\n-- Definition of SDO_GEOMETRY (conceptual; defined by MDSYS)\nCREATE TYPE sdo_geometry AS OBJECT (\n sdo_gtype NUMBER, -- geometry type code\n sdo_srid NUMBER, -- coordinate reference system (EPSG code)\n sdo_point SDO_POINT_TYPE, -- shortcut for 2D/3D point types\n sdo_elem_info SDO_ELEM_INFO_ARRAY, -- element description array\n sdo_ordinates SDO_ORDINATE_ARRAY -- packed coordinate array\n);\n```\n\n### SDO_GTYPE: Geometry Type Codes\n\nThe `SDO_GTYPE` is a 4-digit code: **DLTT**\n- **D**: number of dimensions (2, 3, 4)\n- **L**: LRS (Linear Reference System) measure dimension (0 if none)\n- **TT**: geometry type (01–07)\n\n| GTYPE | Description |\n|---|---|\n| 2001 | 2D Point |\n| 3001 | 3D Point |\n| 2002 | 2D Line String (polyline) |\n| 2003 | 2D Polygon |\n| 3003 | 3D Polygon |\n| 2004 | 2D Geometry Collection |\n| 2005 | 2D MultiPoint |\n| 2006 | 2D MultiLine |\n| 2007 | 2D MultiPolygon |\n\n---\n\n## Common Geometry Subtypes\n\n### Point\n\n```sql\n-- 2D Point using SDO_POINT (fastest/simplest for point data)\n-- Format: SDO_GEOMETRY(gtype, srid, SDO_POINT_TYPE(x, y, z_or_null), null, null)\nSELECT SDO_GEOMETRY(\n 2001, -- 2D Point\n 4326, -- WGS84 coordinate system (GPS)\n SDO_POINT_TYPE(-122.4194, 37.7749, NULL), -- San Francisco (lon, lat)\n NULL,\n NULL\n) AS sf_location\nFROM DUAL;\n\n-- 3D Point\nSELECT SDO_GEOMETRY(\n 3001, -- 3D Point\n 4326,\n SDO_POINT_TYPE(-122.4194, 37.7749, 52.0), -- with elevation in meters\n NULL,\n NULL\n) FROM DUAL;\n```\n\n### Line String\n\n```sql\n-- 2D LineString (a route or road segment)\n-- SDO_ELEM_INFO: (starting_offset, etype, interpretation)\n-- etype 2 = line string, interpretation 1 = straight segments\nSELECT SDO_GEOMETRY(\n 2002, -- 2D LineString\n 4326, -- WGS84\n NULL,\n SDO_ELEM_INFO_ARRAY(1, 2, 1), -- one line string, straight segments\n SDO_ORDINATE_ARRAY(\n -122.4194, 37.7749, -- point 1 (start)\n -122.4094, 37.7849, -- point 2\n -122.3994, 37.7749 -- point 3 (end)\n )\n) AS route\nFROM DUAL;\n```\n\n### Polygon\n\n```sql\n-- Simple 2D Polygon (closed ring, last point = first point)\n-- etype 1003 = exterior polygon ring, interpretation 1 = straight segments\nSELECT SDO_GEOMETRY(\n 2003, -- 2D Polygon\n 4326,\n NULL,\n SDO_ELEM_INFO_ARRAY(1, 1003, 1), -- exterior ring, straight segments\n SDO_ORDINATE_ARRAY(\n -122.45, 37.75, -- SW corner\n -122.40, 37.75, -- SE corner\n -122.40, 37.80, -- NE corner\n -122.45, 37.80, -- NW corner\n -122.45, 37.75 -- close ring (same as first point)\n )\n) AS sf_district\nFROM DUAL;\n\n-- Polygon with a hole (donut shape)\nSELECT SDO_GEOMETRY(\n 2003,\n 4326,\n NULL,\n SDO_ELEM_INFO_ARRAY(\n 1, 1003, 1, -- outer ring starts at ordinate position 1\n 11, 2003, 1 -- inner ring (hole) starts at ordinate position 11\n ),\n SDO_ORDINATE_ARRAY(\n -- Outer ring (5 points = 10 ordinates)\n 0, 0, 10, 0, 10, 10, 0, 10, 0, 0,\n -- Inner ring / hole (5 points = 10 ordinates)\n 2, 2, 8, 2, 8, 8, 2, 8, 2, 2\n )\n) AS donut_polygon\nFROM DUAL;\n```\n\n---\n\n## Setting Up Spatial Tables\n\n### Create Table and Populate USER_SDO_GEOM_METADATA\n\n**Every spatial table must have an entry in `USER_SDO_GEOM_METADATA`** before a spatial index can be created. This metadata defines the valid coordinate range.\n\n```sql\n-- Create the table\nCREATE TABLE store_locations (\n store_id NUMBER PRIMARY KEY,\n store_name VARCHAR2(100),\n city VARCHAR2(50),\n location MDSYS.SDO_GEOMETRY\n);\n\n-- Register spatial metadata (REQUIRED before creating spatial index)\n-- diminfo: array of dimension info: (name, min_val, max_val, tolerance)\n-- tolerance: smallest meaningful distance in the coordinate units (degrees for WGS84)\nINSERT INTO user_sdo_geom_metadata (table_name, column_name, diminfo, srid)\nVALUES (\n 'STORE_LOCATIONS',\n 'LOCATION',\n SDO_DIM_ARRAY(\n SDO_DIM_ELEMENT('LONGITUDE', -180, 180, 0.00001), -- ~1 meter in degrees\n SDO_DIM_ELEMENT('LATITUDE', -90, 90, 0.00001)\n ),\n 4326 -- WGS84 (GPS coordinates)\n);\nCOMMIT;\n\n-- Insert some sample stores\nINSERT INTO store_locations VALUES (1, 'SF Downtown', 'San Francisco',\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4194, 37.7749, NULL), NULL, NULL));\n\nINSERT INTO store_locations VALUES (2, 'Oakland Uptown', 'Oakland',\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.2711, 37.8044, NULL), NULL, NULL));\n\nINSERT INTO store_locations VALUES (3, 'San Jose Center', 'San Jose',\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-121.8863, 37.3382, NULL), NULL, NULL));\n\nCOMMIT;\n```\n\n---\n\n## Spatial Indexes\n\nOracle's spatial index is an R-tree index (or quadtree for certain cases). It is created using the `INDEXTYPE IS MDSYS.SPATIAL_INDEX` syntax.\n\n```sql\n-- Create spatial index (must have metadata registered first)\nCREATE INDEX idx_store_locations_geom\n ON store_locations (location)\n INDEXTYPE IS MDSYS.SPATIAL_INDEX\n PARAMETERS ('sdo_indx_dims=2');\n\n-- For 3D spatial data\nCREATE INDEX idx_buildings_3d\n ON buildings (geom_col)\n INDEXTYPE IS MDSYS.SPATIAL_INDEX_V2\n PARAMETERS ('sdo_indx_dims=3');\n\n-- Verify index creation\nSELECT index_name, status, ityp_owner, ityp_name\nFROM user_indexes\nWHERE table_name = 'STORE_LOCATIONS';\n```\n\n### Spatial Index Rebuild\n\n```sql\n-- Rebuild a spatial index (after bulk loads)\nALTER INDEX idx_store_locations_geom REBUILD;\n\n-- Check spatial index validity\nSELECT sdo_index_name, sdo_index_type, sdo_index_status\nFROM mdsys.sdo_index_info_table\nWHERE sdo_index_table_name = 'STORE_LOCATIONS';\n```\n\n---\n\n## Spatial Operators\n\nOracle spatial uses **operators** (not functions) for primary spatial predicates. The optimizer uses these operators to leverage the spatial index.\n\n### SDO_RELATE: General Topological Relationship\n\n`SDO_RELATE` tests the topological relationship between two geometries using the 9-intersection model (DE-9IM).\n\n```sql\n-- Find all stores within a district boundary polygon\nSELECT s.store_id, s.store_name\nFROM store_locations s,\n district_boundaries d\nWHERE d.district_name = 'Bay Area'\n AND SDO_RELATE(\n s.location, -- geometry 1 (indexed column)\n d.boundary, -- geometry 2\n 'mask=INSIDE' -- relationship mask\n ) = 'TRUE';\n```\n\n**Relationship Masks:**\n\n| Mask | Description |\n|---|---|\n| `TOUCH` | Boundaries touch, interiors don't intersect |\n| `OVERLAPBDYDISJOINT` | Overlap with disjoint boundaries |\n| `OVERLAPBDYINTERSECT` | Overlap with intersecting boundaries |\n| `EQUAL` | Geometrically equal |\n| `INSIDE` | Geometry 1 is inside geometry 2 |\n| `COVEREDBY` | Geometry 1 is covered by (or inside) geometry 2 |\n| `CONTAINS` | Geometry 1 contains geometry 2 |\n| `COVERS` | Geometry 1 covers (or contains) geometry 2 |\n| `ANYINTERACT` | Any interaction (most commonly used) |\n| `ON` | Geometry 1 is on boundary of geometry 2 |\n\n```sql\n-- ANYINTERACT: find any geometries that touch, overlap, or contain each other\nSELECT s.store_id, s.store_name\nFROM store_locations s,\n flood_zones f\nWHERE f.risk_level = 'HIGH'\n AND SDO_RELATE(s.location, f.boundary, 'mask=ANYINTERACT') = 'TRUE';\n\n-- Multiple masks combined with +\nSELECT * FROM parcel_map p, utility_lines u\nWHERE SDO_RELATE(p.geom, u.geom, 'mask=TOUCH+OVERLAPBDYINTERSECT') = 'TRUE';\n```\n\n### SDO_WITHIN_DISTANCE: Proximity Search\n\n```sql\n-- Find stores within 5 km of a given point (e.g., customer location)\nSELECT s.store_id, s.store_name, s.city\nFROM store_locations s\nWHERE SDO_WITHIN_DISTANCE(\n s.location, -- indexed geometry\n SDO_GEOMETRY(2001, 4326,\n SDO_POINT_TYPE(-122.4000, 37.7700, NULL), NULL, NULL), -- query point\n 'distance=5 unit=km' -- distance spec\n ) = 'TRUE';\n\n-- Order results by actual distance\nSELECT s.store_id, s.store_name,\n SDO_GEOM.SDO_DISTANCE(\n s.location,\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4000, 37.7700, NULL), NULL, NULL),\n 0.001, -- tolerance\n 'unit=km'\n ) AS distance_km\nFROM store_locations s\nWHERE SDO_WITHIN_DISTANCE(\n s.location,\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4000, 37.7700, NULL), NULL, NULL),\n 'distance=5 unit=km'\n ) = 'TRUE'\nORDER BY distance_km;\n```\n\n### SDO_NN: Nearest Neighbor Search\n\n```sql\n-- Find the 3 nearest stores to a customer location\nSELECT s.store_id, s.store_name,\n SDO_NN_DISTANCE(1) AS distance_meters\nFROM store_locations s\nWHERE SDO_NN(\n s.location,\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4000, 37.7700, NULL), NULL, NULL),\n 'sdo_num_res=3 unit=meter',\n 1 -- correlation number (must match SDO_NN_DISTANCE argument)\n ) = 'TRUE'\nORDER BY distance_meters;\n\n-- SDO_NN with additional filter (stores that are open)\nSELECT s.store_id, s.store_name, SDO_NN_DISTANCE(1) AS dist\nFROM store_locations s\nWHERE SDO_NN(s.location,\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4, 37.77, NULL), NULL, NULL),\n 'sdo_num_res=10', 1) = 'TRUE'\n AND s.is_open = 'Y'\nORDER BY dist\nFETCH FIRST 3 ROWS ONLY;\n```\n\n### SDO_CONTAINS and SDO_INSIDE\n\n```sql\n-- Find all points inside a polygon\nSELECT s.store_id, s.store_name\nFROM store_locations s,\n sales_territories t\nWHERE t.territory_id = 7\n AND SDO_CONTAINS(t.boundary, s.location) = 'TRUE';\n\n-- SDO_INSIDE: reverse of CONTAINS\nSELECT t.territory_name\nFROM store_locations s,\n sales_territories t\nWHERE s.store_id = 42\n AND SDO_INSIDE(s.location, t.boundary) = 'TRUE';\n```\n\n---\n\n## SDO_GEOM Functions: Measurements and Operations\n\n```sql\n-- Calculate distance between two points\nSELECT SDO_GEOM.SDO_DISTANCE(\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-122.4194, 37.7749, NULL), NULL, NULL),\n SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(-118.2437, 34.0522, NULL), NULL, NULL),\n 0.001, -- tolerance\n 'unit=km'\n) AS sf_to_la_km\nFROM DUAL;\n\n-- Calculate area of a polygon\nSELECT SDO_GEOM.SDO_AREA(\n SDO_GEOMETRY(\n 2003, 4326, NULL,\n SDO_ELEM_INFO_ARRAY(1, 1003, 1),\n SDO_ORDINATE_ARRAY(-122.45, 37.75, -122.40, 37.75,\n -122.40, 37.80, -122.45, 37.80, -122.45, 37.75)\n ),\n 0.001, -- tolerance\n 'unit=sq_km' -- square kilometers\n) AS area_sq_km\nFROM DUAL;\n\n-- Calculate length/perimeter\nSELECT SDO_GEOM.SDO_LENGTH(geom, 0.001, 'unit=km') AS length_km\nFROM road_segments\nWHERE road_id = 101;\n\n-- Buffer: create a polygon at a fixed distance from a geometry\nSELECT SDO_GEOM.SDO_BUFFER(\n location,\n 5000, -- 5000 meters\n 0.001 -- tolerance\n) AS five_km_buffer\nFROM store_locations\nWHERE store_id = 1;\n\n-- Union of geometries\nSELECT SDO_GEOM.SDO_UNION(geom_a, geom_b, 0.001) AS merged_geom\nFROM (SELECT a.boundary AS geom_a, b.boundary AS geom_b\n FROM sales_territories a, sales_territories b\n WHERE a.territory_id = 1 AND b.territory_id = 2);\n\n-- Intersection\nSELECT SDO_GEOM.SDO_INTERSECTION(\n polygon_a, polygon_b, 0.001\n) AS intersection_geom\nFROM geometry_pairs;\n```\n\n---\n\n## Coordinate Reference Systems (SRID)\n\nThe **SRID (Spatial Reference Identifier)** defines the coordinate system. Oracle stores the definitions in `MDSYS.SDO_COORD_REF_SYSTEM`.\n\n```sql\n-- Common SRIDs\n-- 4326 = WGS84 (GPS, longitude/latitude in degrees) — most common\n-- 3857 = Web Mercator (Google Maps, OpenStreetMap tiles) — projected, meters\n-- 27700 = British National Grid (meters, UK)\n-- 32610 = WGS84 / UTM Zone 10N (meters, western US)\n\n-- Look up a coordinate system\nSELECT srid, coord_ref_sys_name, coord_ref_sys_kind\nFROM mdsys.sdo_coord_ref_system\nWHERE srid IN (4326, 3857, 32610);\n\n-- Convert between coordinate systems\nSELECT SDO_CS.TRANSFORM(\n location,\n 3857 -- convert from 4326 (WGS84) to 3857 (Web Mercator)\n) AS location_web_mercator\nFROM store_locations\nWHERE store_id = 1;\n\n-- Validate coordinate system of stored data\nSELECT s.store_id, s.location.sdo_srid\nFROM store_locations s;\n```\n\n---\n\n## GeoJSON Integration\n\n```sql\n-- Convert SDO_GEOMETRY to GeoJSON\nSELECT SDO_UTIL.TO_GEOJSON(location) AS geojson\nFROM store_locations\nWHERE store_id = 1;\n-- Returns: {\"type\":\"Point\",\"coordinates\":[-122.4194,37.7749]}\n\n-- Convert GeoJSON to SDO_GEOMETRY\nSELECT SDO_UTIL.FROM_GEOJSON(\n '{\"type\":\"Point\",\"coordinates\":[-122.4194,37.7749]}'\n) AS location\nFROM DUAL;\n\n-- Full feature collection for REST API\nSELECT JSON_ARRAYAGG(\n JSON_OBJECT(\n 'type' VALUE 'Feature',\n 'id' VALUE store_id,\n 'geometry' VALUE JSON(SDO_UTIL.TO_GEOJSON(location)),\n 'properties' VALUE JSON_OBJECT(\n 'name' VALUE store_name,\n 'city' VALUE city\n )\n )\n) AS geojson_collection\nFROM store_locations;\n```\n\n---\n\n## Best Practices\n\n- **Always register `USER_SDO_GEOM_METADATA`** before creating a spatial index. The metadata defines the valid coordinate extent and tolerance.\n- **Use WGS84 (SRID=4326)** for general-purpose geographic data (GPS coordinates). Use projected coordinate systems (UTM, State Plane) when precise metric distances are required.\n- **Set tolerance appropriately**: ~0.00001 degrees (≈1 meter) for geographic data, 0.001 for projected data in meters. Too tight a tolerance causes false \"not equal\" results; too loose conflates nearby features.\n- **Use spatial operators (`SDO_RELATE`, `SDO_NN`)** in WHERE clauses — not spatial functions (`SDO_GEOM.*`) — to leverage the spatial index.\n- **Pre-compute common distances** for frequently compared geometry pairs and store them as regular NUMBER columns with B-tree indexes.\n- **Use `SDO_NN` for nearest-neighbor queries** rather than `SDO_WITHIN_DISTANCE` with large radii, which scans more of the index.\n- **Partition large spatial tables** by geographic region (e.g., by state or country) to enable partition pruning in spatial queries.\n- **Validate geometry before insertion** using `SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT`.\n\n```sql\n-- Validate geometry before insert\nDECLARE\n v_result VARCHAR2(100);\nBEGIN\n v_result := SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT(\n SDO_GEOMETRY(2003, 4326, NULL,\n SDO_ELEM_INFO_ARRAY(1, 1003, 1),\n SDO_ORDINATE_ARRAY(0,0, 1,0, 1,1, 0,1, 0,0)\n ),\n 0.001\n );\n IF v_result != 'TRUE' THEN\n RAISE_APPLICATION_ERROR(-20010, 'Invalid geometry: ' || v_result);\n END IF;\nEND;\n```\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Creating Spatial Index Without Metadata\n\n```sql\n-- WRONG: will fail with ORA-13203\nCREATE INDEX idx_spatial ON stores(location) INDEXTYPE IS MDSYS.SPATIAL_INDEX;\n\n-- RIGHT: insert metadata first, then create index\nINSERT INTO user_sdo_geom_metadata VALUES (...);\nCOMMIT;\nCREATE INDEX idx_spatial ON stores(location) INDEXTYPE IS MDSYS.SPATIAL_INDEX;\n```\n\n### Mistake 2: Swapping Latitude and Longitude\n\nOracle's SDO_GEOMETRY for WGS84 (SRID=4326) uses **(longitude, latitude)** order — not (lat, lon). This is consistent with the mathematical (x, y) convention and the OGC/GeoJSON standard, but opposite to how many people verbally describe coordinates.\n\n```sql\n-- WRONG: latitude first\nSDO_POINT_TYPE(37.7749, -122.4194, NULL) -- this plots in the Atlantic Ocean\n\n-- RIGHT: longitude first, then latitude\nSDO_POINT_TYPE(-122.4194, 37.7749, NULL) -- San Francisco\n```\n\n### Mistake 3: Using SDO_GEOM Functions in WHERE Clause (No Index)\n\n```sql\n-- WRONG: SDO_GEOM.SDO_DISTANCE does not use the spatial index\nWHERE SDO_GEOM.SDO_DISTANCE(s.location, :point, 0.001) \u003c 5000;\n\n-- RIGHT: use SDO_WITHIN_DISTANCE to get index-accelerated search\nWHERE SDO_WITHIN_DISTANCE(s.location, :point, 'distance=5000') = 'TRUE'\n```\n\n### Mistake 4: Not Closing Polygon Rings\n\nA polygon's first and last coordinate pairs must be identical to close the ring. An unclosed ring produces invalid geometry.\n\n### Mistake 5: Wrong Tolerance for Coordinate System\n\nUsing a very small tolerance (e.g., 0.000001) with projected coordinates in meters (where units are large numbers) causes nearly every operation to return unexpected results. Match tolerance to the unit scale of the SRID.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c Spatial and Graph Developer's Guide (SPATL)](https://docs.oracle.com/en/database/oracle/oracle-database/19/spatl/)\n- [Oracle Database 19c SQL Multimedia and Image Reference](https://docs.oracle.com/en/database/oracle/oracle-database/19/imref/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18030,"content_sha256":"94a1e12d54f2f21f82e7ec48643c4b38af07192b19e8f128582dadabb740e0b2"},{"filename":"appdev/sql-property-graph.md","content":"# SQL Property Graphs in Oracle Database\n\n## Overview\n\nOracle SQL Property Graph lets you model and query graph data — vertices (nodes) and edges (relationships) — directly on top of existing relational tables, views, materialized views, or external tables. No data is copied; the graph definition stores only metadata, and queries operate against current table data.\n\nThe core components are:\n- **`CREATE PROPERTY GRAPH`** — defines which tables are vertices and edges, their keys, labels, and properties\n- **`GRAPH_TABLE` operator** — queries the graph using a pattern-matching syntax (`MATCH`) inside regular SQL `SELECT` statements\n\nThis guide covers SQL Property Graph only. For PGX (in-memory graph analytics using PGQL), see the Oracle Graph Server documentation.\n\n> Note: SQL Property Graph (`CREATE PROPERTY GRAPH`, `GRAPH_TABLE`) requires Oracle Database 23ai (23c) or later. It is not available in Oracle 19c.\n\n---\n\n## Underlying Table Setup\n\nA property graph is built on top of ordinary database objects (tables, views, materialized views, external tables, and supported synonyms). In practice, keys should uniquely identify vertices and edges; in `ENFORCED MODE`, Oracle validates this strictly via key/constraint rules.\n\n```sql\n-- Vertex table: one row = one person vertex\nCREATE TABLE persons (\n person_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n name VARCHAR2(100) NOT NULL,\n birthdate DATE,\n height FLOAT DEFAULT ON NULL 0,\n hr_data JSON\n);\n\n-- Vertex table: one row = one university vertex\nCREATE TABLE university (\n id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n name VARCHAR2(100)\n);\n\n-- Edge table: person_a is friends with person_b\nCREATE TABLE friends (\n friendship_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n person_a NUMBER REFERENCES persons(person_id),\n person_b NUMBER REFERENCES persons(person_id),\n meeting_date DATE\n);\n\n-- Edge table: person is a student of a university\nCREATE TABLE student_of (\n s_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n s_person_id NUMBER REFERENCES persons(person_id),\n s_univ_id NUMBER REFERENCES university(id),\n subject VARCHAR2(100)\n);\n```\n\n---\n\n## Creating a Property Graph\n\n```sql\nCREATE [OR REPLACE] PROPERTY GRAPH \u003cgraph_name>\n VERTEX TABLES (\n \u003ctable_name> [AS \u003calias>] [KEY (\u003ccolumns>)]\n [LABEL \u003clabel_name> [PROPERTIES \u003cproperty_spec>]]\n ...\n )\n EDGE TABLES (\n \u003ctable_name> [AS \u003calias>] [KEY (\u003ccolumns>)]\n SOURCE KEY (\u003ccol>) REFERENCES \u003cvertex_table>(\u003ccol>)\n DESTINATION KEY (\u003ccol>) REFERENCES \u003cvertex_table>(\u003ccol>)\n [LABEL \u003clabel_name> [PROPERTIES \u003cproperty_spec>]]\n ...\n )\n [OPTIONS (\u003coption_spec>)];\n```\n\n### Complete Example\n\n```sql\nCREATE OR REPLACE PROPERTY GRAPH students_graph\n VERTEX TABLES (\n -- KEY inferred from PRIMARY KEY constraint when one exists\n persons KEY (person_id)\n LABEL person\n PROPERTIES (person_id, name, birthdate AS dob)\n LABEL person_ht\n PROPERTIES (height),\n university KEY (id)\n -- No explicit LABEL: defaults to label named \"university\" with all columns\n )\n EDGE TABLES (\n friends\n KEY (friendship_id)\n SOURCE KEY (person_a) REFERENCES persons(person_id)\n DESTINATION KEY (person_b) REFERENCES persons(person_id)\n PROPERTIES (friendship_id, meeting_date),\n student_of\n KEY (s_id)\n SOURCE KEY (s_person_id) REFERENCES persons(person_id)\n DESTINATION KEY (s_univ_id) REFERENCES university(id)\n PROPERTIES (subject)\n );\n```\n\n### Key Auto-Inference Rules\n\nThe graph engine automatically infers keys from constraints:\n\n| Situation | Behavior |\n|-----------|----------|\n| Single `PRIMARY KEY` | Used automatically as the element key |\n| Both `PRIMARY KEY` and `UNIQUE` | `PRIMARY KEY` takes precedence |\n| Inferred from `UNIQUE` | Columns must also be `NOT NULL` |\n| No unique constraint | `KEY (...)` must be declared explicitly |\n| Single `FOREIGN KEY` on edge table | SOURCE/DESTINATION keys inferred automatically |\n| Multiple `FOREIGN KEY` constraints | Must declare `SOURCE KEY` and `DESTINATION KEY` explicitly |\n\n---\n\n## PROPERTIES Clause Options\n\n```sql\n-- Expose all columns as properties (default when no label/properties clause given)\nPROPERTIES ARE ALL COLUMNS\n\n-- Expose all columns except specified ones\nPROPERTIES ARE ALL COLUMNS EXCEPT (internal_col, audit_col)\n\n-- Expose specific columns\nPROPERTIES (person_id, name, birthdate)\n\n-- Rename a column as a property\nPROPERTIES (person_id, birthdate AS dob)\n\n-- Expose a column expression as a property\nPROPERTIES (height * 100 AS height_cm)\n\n-- Expose no properties\nNO PROPERTIES\n```\n\n**Supported property types:** All Oracle built-in types including `VARCHAR2`, `NUMBER`, `DATE`, `TIMESTAMP`, `CLOB`, `BLOB`, `JSON`, virtual columns, and SQL/XML value expressions returning a supported type.\n\n**Not supported directly as properties:** `XMLType` columns, `SDO_GEOMETRY` built-in functions, `ANYTYPE`, user-defined object types, pseudocolumns. Use a view as a workaround.\n\n---\n\n## OPTIONS Clause\n\n```sql\n-- Enforced: key and foreign key constraints validated at graph creation time\nOPTIONS (ENFORCED MODE)\n\n-- Trusted (default): no constraint validation; incorrect data causes incorrect results\nOPTIONS (TRUSTED MODE)\n\n-- Allow properties with same name but different types across labels\nOPTIONS (ALLOW MIXED PROPERTY TYPES)\n\n-- Reject mixed types across labels (default)\nOPTIONS (DISALLOW MIXED PROPERTY TYPES)\n\n-- Combine options\nOPTIONS (ENFORCED MODE, ALLOW MIXED PROPERTY TYPES)\n```\n\n`ENFORCED MODE` guarantees unique `VERTEX_ID`/`EDGE_ID` values and errors early if constraints are missing. It is **not supported** when graph element tables are views.\n\n---\n\n## Querying with GRAPH_TABLE\n\n`GRAPH_TABLE` is a table operator used in the `FROM` clause of any SQL `SELECT`. It returns a relational result set from a graph pattern match.\n\n```sql\nSELECT \u003ccolumns>\nFROM GRAPH_TABLE (\n \u003cgraph_name>\n MATCH\n \u003cpath_pattern> [, \u003cpath_pattern>, ...]\n [WHERE \u003ccondition>]\n [ONE ROW PER MATCH | ONE ROW PER VERTEX (v) | ONE ROW PER STEP (v1, e, v2)]\n COLUMNS (\u003coutput_columns>)\n);\n```\n\n### Basic Path Query\n\n```sql\n-- Find all direct friendships\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (\n a.name AS person_a,\n b.name AS person_b,\n e.meeting_date AS met_on\n )\n);\n```\n\n### Edge Direction Patterns\n\n| Pattern | Meaning |\n|---------|---------|\n| `(a) -[e]-> (b)` | Directed: a to b |\n| `(a) \u003c-[e]- (b)` | Directed: b to a |\n| `(a) -[e]- (b)` or `(a) \u003c-[e]-> (b)` | Undirected (either direction) |\n| `(a) -> (b)` | Directed, anonymous edge |\n| `(a) - (b)` | Undirected, anonymous edge |\n\n### Label Expressions\n\n```sql\n-- Single label\n(a IS person)\n\n-- Label disjunction (any of)\n(a IS person|university)\n\n-- Anonymous vertex (any label)\n(a)\n\n-- Anonymous edge (any label)\n-[]-\n\n-- Edge with label disjunction\n-[e IS friends|student_of]->\n```\n\n### Inline WHERE Filter on Elements\n\n```sql\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH\n (a IS person WHERE a.name = 'John')\n -[e IS student_of WHERE e.subject = 'Computer Science']->\n (b IS university)\n COLUMNS (a.name AS student, b.name AS university, e.subject)\n);\n```\n\n### WHERE Clause on Full Match\n\n```sql\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n WHERE a.birthdate > DATE '1990-01-01'\n AND b.name != a.name\n COLUMNS (a.name AS from_person, b.name AS to_person, e.meeting_date)\n);\n```\n\n### Multiple Path Patterns (JOIN Semantics)\n\n```sql\n-- Shared variable 'a' acts as a natural join between patterns\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH\n (a IS person WHERE a.name = 'John') -[e1 IS friends]-> (b IS person),\n (a IS person WHERE a.name = 'John') -[e2 IS student_of]-> (c IS university)\n COLUMNS (\n a.name AS student,\n b.name AS friend,\n c.name AS school\n )\n);\n-- Patterns with no shared variables produce a cross product\n```\n\n---\n\n## Variable-Length (Quantified) Path Patterns\n\nQuantifiers match repeated edge hops without spelling out each hop explicitly.\n\n```sql\n-- Exactly 2 hops\n(a IS person) -[IS friends]->{2} (b IS person)\n\n-- Between 1 and 3 hops\n(a IS person) -[IS friends]->{1,3} (b IS person)\n\n-- Up to 4 hops (0 to 4)\n(a IS person) -[IS friends]->{,4} (b IS person)\n```\n\n### Aggregating Over Variable-Length Paths\n\n```sql\n-- Collect all person IDs reachable in 1-3 hops from John\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person WHERE a.name = 'John') -[e IS friends]->{1,3} (b IS person)\n COLUMNS (\n a.name AS source,\n LISTAGG(b.person_id, ',') AS reached_ids,\n COUNT(b.person_id) AS hop_count\n )\n);\n```\n\n> Note: SQL graph query limitations (for example, support around path constructs and advanced clauses) can vary by release update. Confirm against your exact version's \"Supported Features and Limitations for Querying a SQL Property Graph\" page.\n\n---\n\n## ONE ROW PER Clause\n\nControls granularity of output rows when iterating over matched paths.\n\n```sql\n-- Default: one row per complete matched path\nONE ROW PER MATCH\n\n-- One row per vertex traversed in path (iterator variable v)\nONE ROW PER VERTEX (v)\n\n-- One row per edge (step) traversed in path (iterator variables v1, e, v2)\nONE ROW PER STEP (v1, e, v2)\n\n-- Position within the current traversed path\nELEMENT_NUMBER(v) -- returns sequential position in the current path\n```\n\n```sql\n-- Emit one row per hop in a variable-length traversal\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person WHERE a.name = 'John') -[e IS friends]->{1,3} (b IS person)\n ONE ROW PER STEP (v1, edge_var, v2)\n COLUMNS (\n ELEMENT_NUMBER(edge_var) AS hop_num,\n v1.name AS from_node,\n v2.name AS to_node\n )\n);\n```\n\n---\n\n## Vertex and Edge Identity Functions\n\n```sql\n-- VERTEX_ID and EDGE_ID return a JSON object uniquely identifying an element\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (\n VERTEX_ID(a) AS a_id, -- {\"GRAPH_OWNER\":\"GRAPHUSER\",\"GRAPH_NAME\":\"STUDENTS_GRAPH\",\"ELEM_TABLE\":\"PERSONS\",\"KEY_VALUE\":{\"PERSON_ID\":1}}\n EDGE_ID(e) AS e_id,\n VERTEX_ID(b) AS b_id\n )\n);\n\n-- Test equality of two element variables\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[]-> (b IS person) -[]-> (c IS person)\n WHERE NOT VERTEX_EQUAL(a, c) -- exclude cycles back to start\n COLUMNS (a.name, b.name, c.name)\n);\n```\n\n`VERTEX_ID`/`EDGE_ID` return `NULL` if the variable is not bound. In `TRUSTED MODE`, duplicate identifiers are possible if key columns lack a unique constraint.\n\n---\n\n## Other Predicates and Functions\n\n```sql\n-- Label membership test\nWHERE n IS LABELED person\nWHERE n IS NOT LABELED university\n\n-- Property existence test\nWHERE PROPERTY_EXISTS(n, 'birthdate')\n\n-- Edge direction test (useful with undirected edges)\nCOLUMNS (\n CASE WHEN a IS SOURCE OF e THEN 'outbound' ELSE 'inbound' END AS direction\n)\n\n-- Select all properties from a vertex or edge\nCOLUMNS (a.*, e.*)\n\n-- Count bindings to a variable in quantified patterns\nCOLUMNS (binding_count(v) AS times_visited)\n```\n\n---\n\n## JSON Properties\n\nJSON columns are fully supported as properties. Use Oracle dot-notation or `JSON_VALUE` to expose nested JSON fields.\n\n```sql\nCREATE PROPERTY GRAPH hr_graph\n VERTEX TABLES (\n persons KEY (person_id)\n LABEL person\n PROPERTIES (\n person_id,\n name,\n -- dot-notation with type conversion method\n hr_data.department.string() AS department,\n -- JSON_VALUE expression\n JSON_VALUE(hr_data, '$.role') AS role\n )\n )\n EDGE TABLES (\n friends\n KEY (friendship_id)\n SOURCE KEY (person_a) REFERENCES persons(person_id)\n DESTINATION KEY (person_b) REFERENCES persons(person_id)\n PROPERTIES (meeting_date)\n );\n\n-- Query with JSON filter\nSELECT *\nFROM GRAPH_TABLE (hr_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n WHERE a.department = 'Engineering'\n COLUMNS (a.name, a.role, b.name AS friend)\n);\n```\n\n**Supported scalar type conversion methods:** `.string()`, `.number()`, `.float()`, `.double()`, `.date()`, `.timestamp()`, `.binary()`\n\n---\n\n## Temporal Queries\n\n```sql\n-- Query the graph as it existed at a specific SCN\nSELECT *\nFROM GRAPH_TABLE (students_graph AS OF SCN 2117789\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (a.name AS a, b.name AS b, e.meeting_date AS met_on)\n);\n\n-- Query as of a specific timestamp\nSELECT *\nFROM GRAPH_TABLE (students_graph AS OF TIMESTAMP SYSTIMESTAMP - INTERVAL '1' HOUR\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (a.name AS a, b.name AS b)\n);\n```\n\n---\n\n## Bind Variables\n\n```sql\n-- Bind variable in GRAPH_TABLE WHERE clause\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person WHERE b.name = :target_name)\n WHERE a.name = :source_name\n COLUMNS (a.name AS person_a, b.name AS person_b, e.meeting_date AS met_on)\n);\n```\n\n---\n\n## DDL Management\n\n### Drop and Rename\n\n```sql\n-- Drop the graph object only; underlying tables are NOT affected\nDROP PROPERTY GRAPH students_graph;\n\n-- Rename\nRENAME students_graph TO students;\n```\n\n### Revalidate After Schema Changes\n\n```sql\n-- After adding or dropping a column on an underlying table:\nALTER PROPERTY GRAPH students_graph COMPILE;\n```\n\n### Comment\n\n```sql\nCOMMENT ON PROPERTY GRAPH students_graph IS 'Student social network graph';\n```\n\n### Retrieve DDL\n\n```sql\nSELECT DBMS_METADATA.GET_DDL('PROPERTY_GRAPH', 'STUDENTS_GRAPH') FROM DUAL;\n```\n\n---\n\n## Data Dictionary Views\n\nAll views have `USER_`, `ALL_`, and `DBA_` prefixes:\n\n| View | Contents |\n|------|----------|\n| `USER_PROPERTY_GRAPHS` | Graph definitions |\n| `USER_PG_ELEMENTS` | Vertex and edge table entries |\n| `USER_PG_EDGE_RELATIONSHIPS` | Source/destination key columns |\n| `USER_PG_KEYS` | Key column definitions |\n| `USER_PG_LABELS` | Label definitions |\n| `USER_PG_LABEL_PROPERTIES` | Label-to-property mapping |\n| `USER_PG_PROP_DEFINITIONS` | Column expressions backing properties |\n| `USER_PG_ELEMENT_LABELS` | Graph element table to label mapping |\n| `USER_PG_COMMENTS` | Graph comments |\n\n---\n\n## Privileges\n\n```sql\n-- Grant system privileges\nGRANT CREATE PROPERTY GRAPH TO app_developer;\nGRANT CREATE ANY PROPERTY GRAPH TO graph_admin;\nGRANT READ ANY PROPERTY GRAPH TO reporting_user;\nGRANT DROP ANY PROPERTY GRAPH TO graph_admin;\n\n-- Grant object-level read on a specific graph\nGRANT SELECT ON PROPERTY GRAPH students_graph TO reporting_user;\n-- SELECT and READ are aliases; both allow querying the graph\n```\n\n---\n\n## Performance and EXPLAIN PLAN\n\n`GRAPH_TABLE` queries are translated internally to SQL, so standard Oracle query tuning applies:\n\n```sql\n-- EXPLAIN PLAN works directly on GRAPH_TABLE queries\nEXPLAIN PLAN FOR\nSELECT *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (a.name AS a, b.name AS b)\n);\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format => 'ALL'));\n\n-- SQL hints are supported both inside and outside GRAPH_TABLE\nSELECT /*+ PARALLEL(4) */ *\nFROM GRAPH_TABLE (students_graph\n MATCH (a IS person) -[e IS friends]-> (b IS person)\n COLUMNS (a.name AS a, b.name AS b)\n);\n```\n\n**Recommendation:** Create indexes on key and frequently filtered columns in the underlying tables. Because `GRAPH_TABLE` translates to SQL joins, standard B-tree indexes on foreign key and primary key columns typically have the most impact.\n\n---\n\n## Using Views as Graph Element Tables\n\nViews can substitute for tables in any position (vertex or edge). This is the standard workaround for source objects that are not directly supported:\n\n| Scenario | Workaround |\n|----------|------------|\n| `XMLType` columns as properties | Create a view that casts to `VARCHAR2` |\n| `SDO_GEOMETRY` functions as properties | Create a view |\n| PL/SQL function virtual columns | Create a regular view |\n| Hybrid partitioned tables | Create a view |\n| Database links | Create a view over the remote table |\n| Pseudocolumns as keys or properties | Create a view that materializes them |\n\n```sql\n-- Workaround for a PL/SQL function virtual column\nCREATE VIEW persons_v AS\nSELECT person_id, name, birthdate, compute_age(birthdate) AS age\nFROM persons;\n\nCREATE PROPERTY GRAPH age_graph\n VERTEX TABLES (persons_v KEY (person_id) LABEL person PROPERTIES ARE ALL COLUMNS)\n ...;\n```\n\n> Note: `ENFORCED MODE` is not supported when using views as graph element tables. Use `TRUSTED MODE` (the default) and ensure uniqueness manually.\n\n---\n\n## Best Practices\n\n- **Build on constrained tables.** Define `PRIMARY KEY` on vertex tables and `FOREIGN KEY` on edge tables; the graph engine auto-infers keys, removing the need for explicit `KEY` declarations and enabling `ENFORCED MODE`.\n- **Use `ENFORCED MODE` for new graphs on tables.** It validates constraints at creation time and guarantees unique `VERTEX_ID`/`EDGE_ID` values, which prevents silent data correctness issues.\n- **Use `OR REPLACE` when iterating on the graph schema.** Property graph definitions are immutable after creation; `OR REPLACE` is the only way to change them without dropping first, and it preserves previously granted privileges.\n- **Call `ALTER PROPERTY GRAPH ... COMPILE` after any schema change** to the underlying tables, otherwise stale metadata may cause query errors.\n- **Index key and join columns.** The `GRAPH_TABLE` operator translates to SQL; indexes on `SOURCE KEY`, `DESTINATION KEY`, and vertex `KEY` columns directly improve traversal performance.\n- **Prefer inline element-pattern filters** (`(a IS person WHERE a.name = 'John')`) over graph-level `WHERE` for single-element conditions — they help the optimizer push filters closer to the base table scan.\n- **Use `TRUSTED MODE` with views** since `ENFORCED MODE` is not available for views; validate uniqueness of key columns in the view output independently.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Expecting Data to Be Copied\n\nA property graph stores only metadata. It reads from the live underlying tables at query time. Dropping or truncating an underlying table affects all graph queries that use it.\n\n### Mistake 2: Missing KEY on Tables Without Constraints\n\n```sql\n-- WRONG: t1 has no PRIMARY KEY or UNIQUE constraint\nCREATE TABLE t1 (id NUMBER, name VARCHAR2(10));\nCREATE PROPERTY GRAPH g VERTEX TABLES (t1 KEY (id) LABEL t PROPERTIES ARE ALL COLUMNS)\n OPTIONS (ENFORCED MODE);\n-- ORA-42434: Columns used to define a graph element table key must be NOT NULL in ENFORCED MODE\n```\n\n```sql\n-- RIGHT: add NOT NULL and UNIQUE before using ENFORCED MODE\nALTER TABLE t1 MODIFY id NOT NULL;\nALTER TABLE t1 ADD CONSTRAINT t1_pk PRIMARY KEY (id);\n```\n\n### Mistake 3: Forgetting to Recompile After a Schema Change\n\n```sql\n-- After: ALTER TABLE persons ADD email VARCHAR2(200)\n-- the property graph metadata is stale; compile it\nALTER PROPERTY GRAPH students_graph COMPILE;\n```\n\n### Mistake 4: Using `TIMESTAMP WITH TIME ZONE` as a Key Column\n\n`TIMESTAMP WITH TIME ZONE` is not supported as a key data type. Use `TIMESTAMP` (without timezone) or convert at the view level.\n\n### Mistake 5: Applying a Cross-Product Instead of a Join\n\nTwo path patterns with no shared variables produce a cross product, not a join:\n\n```sql\n-- This is a CROSS PRODUCT (no shared variable)\nMATCH (a IS person), (b IS university)\n\n-- This is a JOIN (shared variable 'a')\nMATCH (a IS person) -[IS student_of]-> (c IS university),\n (a IS person) -[IS friends]-> (b IS person)\n```\n\n### Mistake 6: Assuming Every SQL Graph Feature Is Available in Every RU\n\nSQL graph query support has evolved across 23ai and 26ai release updates. Check the release-specific \"Supported Features and Limitations for Querying a SQL Property Graph\" page before relying on advanced clauses.\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- **Oracle 19c:** SQL Property Graph (`CREATE PROPERTY GRAPH`, `GRAPH_TABLE`) is **not available**. Graph processing in 19c requires Oracle Graph Server (PGX) as a separate component using PGQL query language.\n- **Oracle 23ai (23c):** First release supporting SQL Property Graph DDL and the `GRAPH_TABLE` operator natively in the database engine. Core features: `CREATE PROPERTY GRAPH`, `DROP`, `ALTER PROPERTY GRAPH COMPILE`, `RENAME`, `GRAPH_TABLE` with `MATCH`, label expressions, quantified paths (`{n}`, `{n,m}`, `{,m}`), `VERTEX_ID`/`EDGE_ID`, `ONE ROW PER` clause.\n- **Oracle 26ai (26.1):** Continued iteration on SQL Property Graph. The 26.1 documentation notes that SQL property graphs can be created from database views beginning with Oracle Database Release 23.26.1. Verify RU-specific behavior in the \"Key Property Graph Features in Oracle AI Database 26ai\" page.\n- **ENFORCED MODE with views:** Not supported in any release — use `TRUSTED MODE` when graph element tables are views.\n- **`TIMESTAMP WITH TIME ZONE` keys:** Not supported in any release.\n\n---\n\n## Sources\n\n- [Oracle Property Graph Developer's Guide, Release 26.1 — SQL Property Graph](https://docs.oracle.com/en/database/oracle/property-graph/26.1/spgdg/sql-property-graph.html#SPGDG-GUID-B813BA1B-AEA0-4C70-8094-739FFC0E805B)\n- [Oracle Property Graph Developer's Guide, Release 26.1 — Creating a SQL Property Graph](https://docs.oracle.com/en/database/oracle/property-graph/26.1/spgdg/creating-sql-property-graph.html)\n- [Oracle Property Graph Developer's Guide, Release 26.1 — SQL Graph Queries](https://docs.oracle.com/en/database/oracle/property-graph/26.1/spgdg/sql-graph-queries.html)\n- [Oracle Property Graph Developer's Guide, Release 26.1 — Key Property Graph Features in Oracle AI Database 26ai](https://docs.oracle.com/en/database/oracle/property-graph/26.1/spgdg/key-property-graph-features-oracle-ai-database-26ai.html)\n- [Oracle Database SQL Language Reference 23ai — CREATE PROPERTY GRAPH](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/create-property-graph.html)\n- [Oracle Database SQL Language Reference 23ai — ALTER PROPERTY GRAPH](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/alter-property-graph.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22330,"content_sha256":"ab5cc309cd58b44186765a8c08f13d07c848899505256e0da6af3b56656fb3f3"},{"filename":"appdev/transaction-management.md","content":"# Transaction Management in Oracle Database\n\n## Overview\n\nA transaction is a logical unit of work that consists of one or more SQL statements. Oracle's transaction model is one of the most robust in the relational database world, providing full ACID guarantees while enabling high concurrency through its multi-version concurrency control (MVCC) implementation.\n\nUnderstanding how Oracle manages transactions is essential for writing correct, performant applications. Subtle mistakes — uncommitted transactions held too long, improper use of savepoints, or misunderstanding autonomous transactions — are among the most common sources of data corruption, lock contention, and deadlocks.\n\n---\n\n## ACID Properties in Oracle\n\n### Atomicity\n\nEvery statement in a transaction either fully succeeds or fully fails. If a statement fails mid-execution (e.g., a unique constraint violation), Oracle automatically rolls back that single statement's changes via **statement-level rollback** — the transaction itself remains open with prior changes intact.\n\n```sql\n-- Statement-level rollback demonstration\nINSERT INTO orders (order_id, customer_id) VALUES (1, 101); -- succeeds\nINSERT INTO orders (order_id, customer_id) VALUES (1, 102); -- fails: duplicate PK\n-- At this point, the first INSERT is still pending (transaction still open)\n-- The second INSERT was rolled back automatically\nCOMMIT; -- only the first INSERT is committed\n```\n\n### Consistency\n\nOracle enforces all integrity constraints at transaction commit time (by default). You can defer constraint checking within a transaction:\n\n```sql\n-- Defer constraint checking until commit\nALTER TABLE child_table\n MODIFY CONSTRAINT fk_parent DEFERRABLE INITIALLY DEFERRED;\n\n-- Within a session, you can enable deferred checking\nSET CONSTRAINTS ALL DEFERRED;\n\n-- Now you can insert child before parent within the same transaction\nINSERT INTO child_table (id, parent_id) VALUES (1, 999);\nINSERT INTO parent_table (id) VALUES (999);\nCOMMIT; -- constraints checked here; if parent doesn't exist, rollback\n```\n\n### Isolation\n\nOracle implements isolation through **MVCC (Multi-Version Concurrency Control)**. Readers do not block writers; writers do not block readers. Each query sees a consistent snapshot of data as of its start time (or the transaction's start time in serializable mode).\n\nOracle supports two standard isolation levels:\n\n| Level | Description | Oracle Default? |\n|---|---|---|\n| `READ COMMITTED` | Each statement sees data committed before the statement began | Yes |\n| `SERIALIZABLE` | Transaction sees data as of its start; errors if data changed since then | No (must set explicitly) |\n\n```sql\n-- Set serializable isolation for the current transaction\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE;\n\n-- Or for the session\nALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;\n```\n\nNote: Oracle does **not** support `READ UNCOMMITTED` (dirty reads). This is by design — MVCC eliminates the need for it.\n\n### Durability\n\nOnce `COMMIT` returns successfully, Oracle guarantees the data is written to the redo log on disk. The redo log writer (LGWR) must flush log entries to disk before COMMIT returns.\n\n```sql\n-- Force a synchronous redo write (default behavior)\nCOMMIT WRITE IMMEDIATE WAIT;\n\n-- Asynchronous commit (higher throughput, slightly reduced durability window)\nCOMMIT WRITE IMMEDIATE NOWAIT;\n\n-- Batch commit (best for bulk loads where some loss is acceptable)\nCOMMIT WRITE BATCH NOWAIT;\n```\n\n---\n\n## Starting, Committing, and Rolling Back\n\nOracle starts a transaction implicitly with the first DML statement. There is no explicit `BEGIN TRANSACTION` in SQL*Plus or most tools (PL/SQL has `BEGIN ... END` blocks, but that is not a transaction boundary).\n\n```sql\n-- Transaction starts implicitly with first DML\nINSERT INTO accounts (account_id, balance) VALUES (1001, 5000);\nUPDATE accounts SET balance = balance - 500 WHERE account_id = 1001;\nUPDATE accounts SET balance = balance + 500 WHERE account_id = 2001;\n\n-- Commit makes changes permanent and releases all locks\nCOMMIT;\n\n-- Rollback discards all changes since last commit and releases locks\nROLLBACK;\n```\n\n### Commit Best Practices\n\n```sql\n-- Good: commit after a logical unit of work\nBEGIN\n -- Transfer funds atomically\n UPDATE accounts SET balance = balance - p_amount\n WHERE account_id = p_from_account;\n\n UPDATE accounts SET balance = balance + p_amount\n WHERE account_id = p_to_account;\n\n INSERT INTO transfer_log (from_acct, to_acct, amount, transfer_date)\n VALUES (p_from_account, p_to_account, p_amount, SYSDATE);\n\n COMMIT; -- all three changes committed together\nEND;\n```\n\nA benefit of PL/SQL is that a PL/SQL block is also a statement level transaction. If any of the three DMLs above had failed, all three will be rolled back automatically because the entire PL/SQL block is deemed a statement level transaction.\n\n---\n\n## Savepoints\n\nSavepoints allow partial rollbacks within a transaction. A `ROLLBACK TO savepoint_name` undoes all changes made after the savepoint was established, but does **not** end the transaction — prior changes and the savepoint itself remain.\n\n```sql\nINSERT INTO orders (order_id, status) VALUES (1001, 'PENDING');\nSAVEPOINT after_order;\n\nINSERT INTO order_items (order_id, item_id, qty) VALUES (1001, 'WIDGET', 5);\nSAVEPOINT after_first_item;\n\nINSERT INTO order_items (order_id, item_id, qty) VALUES (1001, 'GADGET', 2);\n-- Suppose this item is out of stock, remove it but keep the order and first item\nROLLBACK TO after_first_item;\n\n-- Transaction still open; order and first item are still pending\nCOMMIT; -- commits order + first item only\n```\n\n### Savepoints in PL/SQL Error Handling\n\n```plpgsql\nDECLARE\n e_invalid_item EXCEPTION;\nBEGIN\n INSERT INTO orders (order_id, customer_id, order_date)\n VALUES (seq_orders.NEXTVAL, 42, SYSDATE);\n\n SAVEPOINT order_created;\n\n FOR item IN (SELECT * FROM staging_order_items WHERE session_id = :sid) LOOP\n BEGIN\n INSERT INTO order_items (order_id, product_id, quantity, price)\n VALUES (item.order_id, item.product_id, item.quantity,\n get_current_price(item.product_id));\n EXCEPTION\n WHEN OTHERS THEN\n -- Skip bad items, keep good ones\n ROLLBACK TO order_created;\n log_error('Failed to insert item: ' || item.product_id);\n END;\n END LOOP;\n\n COMMIT;\nEND;\n```\n\n**Important:** `ROLLBACK TO savepoint` releases row locks acquired after the savepoint, but does NOT release locks acquired before it.\n\n---\n\n## Autonomous Transactions\n\nAn **autonomous transaction** is an independent transaction spawned from within a calling transaction. It has its own commit/rollback scope — completely independent of the parent transaction. Changes made by an autonomous transaction are visible to other sessions immediately after its commit, even if the calling transaction has not committed.\n\nEnable with the `PRAGMA AUTONOMOUS_TRANSACTION` compiler directive.\n\n### Primary Use Cases\n\n1. **Error logging** — record errors even when the calling transaction rolls back\n2. **Audit trails** — insert audit records that must survive a rollback\n3. **Sequence number generation** — in rare cases where gaps must be tracked\n\n```sql\n-- Audit/logging procedure using autonomous transaction\nCREATE OR REPLACE PROCEDURE log_audit_event (\n p_action IN VARCHAR2,\n p_table IN VARCHAR2,\n p_row_id IN VARCHAR2,\n p_details IN VARCHAR2\n) AS\n PRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\n INSERT INTO audit_log (log_id, action, table_name, row_id, details, log_time, logged_by)\n VALUES (seq_audit.NEXTVAL, p_action, p_table, p_row_id, p_details, SYSTIMESTAMP, USER);\n\n COMMIT; -- REQUIRED: autonomous transaction must be explicitly committed or rolled back\nEND log_audit_event;\n/\n\n-- Usage: even if the outer transaction rolls back, the audit record persists\nBEGIN\n UPDATE sensitive_data SET value = 'changed' WHERE id = 42;\n log_audit_event('UPDATE', 'SENSITIVE_DATA', '42', 'Value changed');\n -- If we rollback here, log_audit_event's INSERT is already committed\n ROLLBACK; -- sensitive_data change is undone, but audit record remains\nEND;\n```\n\n### Error Logging Table Pattern\n\n```sql\nCREATE TABLE error_log (\n log_id NUMBER GENERATED ALWAYS AS IDENTITY,\n error_msg VARCHAR2(4000),\n error_code NUMBER,\n program VARCHAR2(100),\n log_time TIMESTAMP DEFAULT SYSTIMESTAMP,\n CONSTRAINT pk_error_log PRIMARY KEY (log_id)\n);\n\nCREATE OR REPLACE PROCEDURE log_error (\n p_msg IN VARCHAR2,\n p_code IN NUMBER DEFAULT SQLCODE,\n p_program IN VARCHAR2 DEFAULT $PLSQL_UNIT\n) AS\n PRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\n INSERT INTO error_log (error_msg, error_code, program)\n VALUES (SUBSTR(p_msg, 1, 4000), p_code, p_program);\n COMMIT;\nEND log_error;\n```\n\n### Autonomous Transaction Gotchas\n\n```sql\n-- WRONG: Autonomous transaction reading uncommitted data from parent\n-- Parent inserts a row but hasn't committed\n-- Autonomous transaction CANNOT see parent's uncommitted changes\nCREATE OR REPLACE PROCEDURE bad_autonomous AS\n PRAGMA AUTONOMOUS_TRANSACTION;\n v_count NUMBER;\nBEGIN\n -- This reads the COMMITTED state of the table, not parent's pending inserts\n SELECT COUNT(*) INTO v_count FROM orders WHERE status = 'PENDING';\n COMMIT;\nEND;\n```\n\n---\n\n## Distributed Transactions (XA)\n\nOracle supports the X/Open XA protocol for distributed transactions spanning multiple databases or resource managers (databases + message queues).\n\n### Two-Phase Commit (2PC)\n\nOracle acts as either a **coordinator** or a **participant** in 2PC:\n\n```sql\n-- Phase 1: Prepare (coordinator asks all participants to prepare)\n-- Each participant writes prepared state to its redo log\n\n-- Phase 2: Commit or Rollback (coordinator decision)\n-- All participants commit or all rollback\n\n-- Monitoring in-doubt distributed transactions\nSELECT local_tran_id, global_tran_id, state, mixed, advice, tran_comment\nFROM dba_2pc_pending;\n\n-- Force commit of an in-doubt transaction (after network recovery)\nCOMMIT FORCE 'local_tran_id';\n\n-- Force rollback of an in-doubt transaction\nROLLBACK FORCE 'local_tran_id';\n```\n\n### Database Links in Transactions\n\n```sql\n-- Transactions automatically become distributed when they touch a DB link\nUPDATE orders@remote_db SET status = 'SHIPPED' WHERE order_id = :id;\nUPDATE local_inventory SET qty = qty - 1 WHERE product_id = :prod;\nCOMMIT; -- Oracle automatically performs 2PC with remote_db\n```\n\n### JDBC XA Example\n\n```java\nimport javax.sql.XADataSource;\nimport javax.transaction.xa.XAResource;\nimport javax.transaction.xa.Xid;\n\n// XA with JTA transaction manager (e.g., Atomikos, Bitronix, Narayana)\nXAConnection xaConn = xaDataSource.getXAConnection(\"user\", \"password\");\nXAResource xaResource = xaConn.getXAResource();\n\nXid xid = createXid(); // application-defined transaction ID\n\nxaResource.start(xid, XAResource.TMNOFLAGS);\ntry (Connection conn = xaConn.getConnection()) {\n conn.prepareStatement(\"UPDATE accounts SET balance = balance - 100 WHERE id = 1\")\n .executeUpdate();\n}\nxaResource.end(xid, XAResource.TMSUCCESS);\n\nint prepareResult = xaResource.prepare(xid);\nif (prepareResult == XAResource.XA_OK) {\n xaResource.commit(xid, false);\n}\n```\n\n---\n\n## Avoiding Long-Running Transactions\n\nLong-running transactions are the most common source of performance problems in Oracle applications. They:\n\n- Hold row locks that block other sessions\n- Generate undo data that must be retained until the transaction completes\n- Can cause `ORA-01555: snapshot too old` for other long-running queries\n- Consume undo tablespace, potentially causing ORA-30036\n\n### Detecting Long-Running Transactions\n\n```sql\n-- Find sessions with open transactions\nSELECT s.sid, s.serial#, s.username, s.status, s.program,\n t.start_time, t.used_ublk AS undo_blocks_used,\n ROUND((SYSDATE - TO_DATE(t.start_time,'MM/DD/YY HH24:MI:SS')) * 24 * 60, 1)\n AS minutes_open\nFROM v$session s\nJOIN v$transaction t ON s.taddr = t.addr\nORDER BY minutes_open DESC;\n\n-- Check undo usage\nSELECT usn, xacts, rssize/1024/1024 AS mb_used, status\nFROM v$rollstat rs\nJOIN v$rollname rn ON rs.usn = rn.usn\nORDER BY mb_used DESC;\n```\n\n### Batch Processing Pattern — Commit in Batches\n\nFor example, it may be the case that a single large UPDATE holds locks for too long a period\n\n```sql\nUPDATE large_table SET processed_date = SYSDATE WHERE needs_processing = 'Y'\nCOMMIT; -- holds all locks for the entire loop duration\n```\n\nTo avoid this, commit every N rows also ensuring that the cursor is freshly reopened to avoid ORA-1555 errors.\n\n```plpgsql\nDECLARE\n TYPE id_array IS TABLE OF NUMBER;\n v_ids id_array;\n CURSOR c_pending IS\n SELECT id FROM large_table WHERE needs_processing = 'Y';\nBEGIN\n LOOP\n OPEN c_pending;\n FETCH c_pending BULK COLLECT INTO v_ids LIMIT 5000;\n EXIT WHEN v_ids.COUNT = 0;\n\n FORALL i IN 1..v_ids.COUNT\n UPDATE large_table\n SET processed_date = SYSDATE\n WHERE id = v_ids(i);\n\n COMMIT;\n CLOSE c_pending;\n END LOOP;\nEND;\n```\n\n---\n\n## Best Practices\n\n- **Keep transactions as short as possible.** Do all computation before the transaction, execute DML, commit immediately.\n- **Never wait for user input inside a transaction.** The user might walk away, leaving locks held.\n- **Always handle exceptions. Call ROLLBACK if necessary** An unhandled exception in application code that disconnects without rollback may leave the transaction open until the session is killed.\n- **Use `COMMIT WRITE BATCH NOWAIT` only for bulk loads** where you understand the durability trade-off.\n- **Prefer `FORALL` and bulk operations** over row-by-row DML to reduce commit frequency while keeping transactions short.\n- **Test with `SERIALIZABLE` isolation** if your application logic depends on consistent reads across multiple statements; do not assume `READ COMMITTED` provides snapshot consistency at the transaction level.\n- **Never use `PRAGMA AUTONOMOUS_TRANSACTION` to work around locking issues.** It creates invisible data dependencies that are hard to debug.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Swallowing Exceptions Without Rollback\n\n```plpgsql\n-- WRONG: exception is caught but transaction is not rolled back\nBEGIN\n UPDATE accounts SET balance = balance - 500 WHERE id = 1;\n UPDATE accounts SET balance = balance + 500 WHERE id = 2;\nEXCEPTION\n WHEN OTHERS THEN\n -- Silent swallow — first UPDATE may be committed elsewhere\n NULL;\nEND;\n\n-- RIGHT\nBEGIN\n UPDATE accounts SET balance = balance - 500 WHERE id = 1;\n UPDATE accounts SET balance = balance + 500 WHERE id = 2;\n COMMIT;\n -- any error will be raised and both statements rolled back automatically by PL/SQL\nEND;\n\n-- ALTERNATIVE\nBEGIN\n UPDATE accounts SET balance = balance - 500 WHERE id = 1;\n UPDATE accounts SET balance = balance + 500 WHERE id = 2;\n COMMIT;\nEXCEPTION\n WHEN OTHERS THEN\n log_error(...) -- if we want to capture error details\n RAISE; -- re-raise so the caller knows\nEND;\n```\n\n### Mistake 2: DDL in the Middle of a Transaction\n\nIn Oracle, any DDL statement (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`) issues an implicit COMMIT before and after it executes. This silently commits any pending DML.\n\n```sql\nINSERT INTO temp_data VALUES (1, 'test'); -- DML\nCREATE INDEX idx_temp ON temp_data(id); -- IMPLICIT COMMIT before this!\nROLLBACK; -- too late; the INSERT was already committed\n```\n\n### Mistake 3: Relying on Autocommit in JDBC\n\nJDBC connections have `autoCommit=true` by default. Every single statement is immediately committed. This is almost never what you want for OLTP.\n\n```java\n// Disable autocommit immediately after obtaining connection\nconnection.setAutoCommit(false);\n// ... perform DML ...\nconnection.commit(); // or connection.rollback() in catch block\n```\n\n### Mistake 4: Misunderstanding Statement-Level vs. Transaction-Level Rollback\n\nWhen a DML statement fails with an exception, only that statement is rolled back. The transaction remains open. If you catch the exception and do nothing, the prior statements in the transaction are still uncommitted and their locks are still held.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c Concepts (CNCPT) — Transactions](https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/)\n- [Oracle Database 19c Application Developer's Guide (ADFNS)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/)\n- [Oracle Database 19c PL/SQL Language Reference — Transaction Processing](https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17201,"content_sha256":"25c5ea3b1756e0777b2e2c2b9564ba76c7fe7d857d6d727eccde925806fde7f3"},{"filename":"appdev/xml-in-oracle.md","content":"# XML in Oracle Database\n\n## Overview\n\nOracle has provided native XML support since Oracle 9i through the `XMLType` data type and the Oracle XML DB (XMLDB) component. Oracle's XML capabilities span storage, querying with XQuery and XPath, generation, transformation, and indexing. While JSON has largely displaced XML for new application development, XML remains essential for:\n\n- EDI, HIPAA, and government data exchange standards\n- SOAP web service integration\n- Legacy system interfaces\n- Document management and content repositories\n- Configuration storage\n\n---\n\n## XMLType Storage Options\n\nOracle XMLType can be stored in three distinct internal formats, each with different performance trade-offs.\n\n### 1. Object-Relational Storage (Schema-Registered XML)\n\nBest for: Highly structured, frequently queried XML with a stable, known schema. Oracle maps XML elements to relational columns internally, enabling fast XPath navigation.\n\n```sql\n-- Register an XML schema\nBEGIN\n DBMS_XMLSCHEMA.REGISTER_SCHEMA(\n SCHEMAURL => 'http://myapp.com/schemas/order.xsd',\n SCHEMADOC => XMLTYPE(\n '\u003c?xml version=\"1.0\"?>\n \u003cxs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"\n targetNamespace=\"http://myapp.com/schemas/order\">\n \u003cxs:element name=\"Order\">\n \u003cxs:complexType>\n \u003cxs:sequence>\n \u003cxs:element name=\"OrderId\" type=\"xs:integer\"/>\n \u003cxs:element name=\"CustomerName\" type=\"xs:string\"/>\n \u003cxs:element name=\"TotalAmount\" type=\"xs:decimal\"/>\n \u003c/xs:sequence>\n \u003c/xs:complexType>\n \u003c/xs:element>\n \u003c/xs:schema>'\n ),\n LOCAL => TRUE,\n GENTYPES => TRUE,\n GENBEAN => FALSE,\n GENTABLES => FALSE\n );\nEND;\n/\n\n-- Create table with schema-based XMLType\nCREATE TABLE orders_xml (\n order_id NUMBER PRIMARY KEY,\n order_doc XMLType\n)\nXMLTYPE COLUMN order_doc STORE AS OBJECT RELATIONAL\n XMLSCHEMA \"http://myapp.com/schemas/order.xsd\"\n ELEMENT \"Order\";\n```\n\n### 2. CLOB Storage (Unstructured XML)\n\nBest for: Variable-structure XML, XML you mainly store and retrieve whole, legacy data. Simplest to set up.\n\n```sql\n-- XMLType stored as CLOB (unstructured)\nCREATE TABLE contracts (\n contract_id NUMBER PRIMARY KEY,\n contract_xml XMLType\n)\nXMLTYPE COLUMN contract_xml STORE AS CLOB;\n\n-- Insert XML\nINSERT INTO contracts VALUES (\n 1,\n XMLType('\u003cContract>\n \u003cContractId>1001\u003c/ContractId>\n \u003cParties>\n \u003cParty role=\"buyer\">Acme Corp\u003c/Party>\n \u003cParty role=\"seller\">Beta LLC\u003c/Party>\n \u003c/Parties>\n \u003cEffectiveDate>2025-01-01\u003c/EffectiveDate>\n \u003cValue currency=\"USD\">50000\u003c/Value>\n \u003c/Contract>')\n);\n\n-- Or from a string variable\nDECLARE\n v_xml CLOB := '\u003cContract>\u003cContractId>1002\u003c/ContractId>\u003c/Contract>';\nBEGIN\n INSERT INTO contracts VALUES (1002, XMLType(v_xml));\n COMMIT;\nEND;\n```\n\n### 3. Binary XML Storage (Recommended for 11g+)\n\nBest for: General-purpose XML storage without schema registration. Stores in a compact binary post-parse format (similar to conceptual approach as JSON's OSON). Faster than CLOB, simpler than object-relational.\n\n```sql\nCREATE TABLE product_specs (\n product_id NUMBER PRIMARY KEY,\n spec_xml XMLType\n)\nXMLTYPE COLUMN spec_xml STORE AS BINARY XML;\n\n-- Insert XML\nINSERT INTO product_specs VALUES (\n 101,\n XMLType('\u003cSpecification>\n \u003cProductId>101\u003c/ProductId>\n \u003cName>Industrial Widget\u003c/Name>\n \u003cDimensions unit=\"mm\">\n \u003cWidth>150\u003c/Width>\n \u003cHeight>75\u003c/Height>\n \u003cDepth>50\u003c/Depth>\n \u003c/Dimensions>\n \u003cMaterials>\n \u003cMaterial>Steel\u003c/Material>\n \u003cMaterial>Rubber\u003c/Material>\n \u003c/Materials>\n \u003c/Specification>')\n);\n```\n\n---\n\n## XPath Extraction with XMLType Methods\n\n```sql\n-- Extract a single node value using XPath\nSELECT EXTRACTVALUE(spec_xml, '/Specification/Name') AS product_name\nFROM product_specs;\n\n-- Extract an XML fragment (returns XMLType)\nSELECT EXTRACT(spec_xml, '/Specification/Dimensions') AS dimensions_xml\nFROM product_specs;\n\n-- existsNode: test for node existence\nSELECT product_id\nFROM product_specs\nWHERE EXISTSNODE(spec_xml, '/Specification/Materials/Material[text()=\"Steel\"]') = 1;\n\n-- XMLQuery: XQuery evaluation returning XMLType\nSELECT XMLQuery('$x/Specification/Name/text()'\n PASSING spec_xml AS \"x\"\n RETURNING CONTENT) AS name_value\nFROM product_specs;\n```\n\nNote: `EXTRACTVALUE`, `EXTRACT`, and `EXISTSNODE` are deprecated in 11g+. Prefer `XMLQUERY`, `XMLEXISTS`, and `XMLTABLE`.\n\n---\n\n## XMLTable: Shredding XML into Relational Rows\n\n`XMLTable` is the modern, recommended way to convert XML documents into relational data. It uses XQuery path expressions to navigate the document.\n\n```sql\n-- Basic XMLTable usage\nSELECT x.product_id_val, x.product_name, x.width, x.height\nFROM product_specs p,\n XMLTable('/Specification'\n PASSING p.spec_xml\n COLUMNS\n product_id_val NUMBER PATH 'ProductId',\n product_name VARCHAR2(200) PATH 'Name',\n width NUMBER PATH 'Dimensions/Width',\n height NUMBER PATH 'Dimensions/Height'\n ) x;\n\n-- Expand repeating elements (array-like)\nSELECT p.product_id, m.material_name\nFROM product_specs p,\n XMLTable('/Specification/Materials/Material'\n PASSING p.spec_xml\n COLUMNS\n material_name VARCHAR2(100) PATH '.'\n ) m;\n\n-- Extract attribute values\nSELECT x.currency, x.value\nFROM contracts c,\n XMLTable('/Contract/Value'\n PASSING c.contract_xml\n COLUMNS\n currency VARCHAR2(3) PATH '@currency', -- @ for attributes\n value NUMBER(15,2) PATH '.'\n ) x;\n\n-- Nested XMLTable for hierarchical data\nSELECT p.product_id, outer_x.dim_unit, inner_x.dim_name, inner_x.dim_value\nFROM product_specs p,\n XMLTable('/Specification/Dimensions'\n PASSING p.spec_xml\n COLUMNS\n dim_unit VARCHAR2(10) PATH '@unit',\n dim_xml XMLType PATH '.'\n ) outer_x,\n XMLTable('/*'\n PASSING outer_x.dim_xml\n COLUMNS\n dim_name VARCHAR2(50) PATH 'fn:name(.)',\n dim_value NUMBER PATH '.'\n ) inner_x;\n```\n\n### XMLTable with Namespaces\n\n```sql\n-- XML with namespace declarations\nSELECT x.order_id, x.customer\nFROM XMLTable(\n XMLNAMESPACES('http://orders.example.com' AS \"ord\"),\n '/ord:OrderSet/ord:Order'\n PASSING XMLType(\n '\u003cOrderSet xmlns=\"http://orders.example.com\">\n \u003cOrder>\u003cOrderId>1\u003c/OrderId>\u003cCustomer>Acme\u003c/Customer>\u003c/Order>\n \u003cOrder>\u003cOrderId>2\u003c/OrderId>\u003cCustomer>Beta\u003c/Customer>\u003c/Order>\n \u003c/OrderSet>'\n )\n COLUMNS\n order_id NUMBER PATH 'ord:OrderId',\n customer VARCHAR2(100) PATH 'ord:Customer'\n ) x;\n```\n\n---\n\n## Generating XML: XMLElement, XMLForest, XMLAgg\n\nOracle provides functions to generate XML from relational data.\n\n```sql\n-- XMLElement: create an XML element from a value\nSELECT XMLElement(\"Employee\",\n XMLElement(\"Name\", first_name || ' ' || last_name),\n XMLElement(\"Department\", department_id),\n XMLElement(\"Salary\", salary)\n ).getClobVal() AS employee_xml\nFROM employees\nWHERE department_id = 10;\n\n-- XMLForest: create a sequence of elements from columns\nSELECT XMLElement(\"Employee\",\n XMLForest(\n employee_id AS \"Id\",\n first_name AS \"FirstName\",\n last_name AS \"LastName\",\n hire_date AS \"HireDate\"\n )\n ).getClobVal() AS emp_xml\nFROM employees;\n\n-- XMLAttributes: add attributes to an element\nSELECT XMLElement(\"Product\",\n XMLAttributes(\n product_id AS \"id\",\n 'active' AS \"status\"\n ),\n XMLForest(\n product_name AS \"Name\",\n list_price AS \"Price\"\n )\n ).getClobVal() AS product_xml\nFROM products;\n\n-- XMLAgg: aggregate multiple XML elements into one parent\nSELECT XMLElement(\"Department\",\n XMLAttributes(department_id AS \"id\"),\n XMLAgg(\n XMLElement(\"Employee\",\n XMLForest(\n employee_id AS \"Id\",\n first_name AS \"Name\"\n )\n )\n ORDER BY last_name\n )\n ).getClobVal() AS dept_xml\nFROM employees\nGROUP BY department_id;\n```\n\n### XMLRoot and XMLDocument\n\n```sql\n-- Add XML declaration and root element\nSELECT XMLRoot(\n XMLElement(\"Employees\",\n XMLAgg(XMLElement(\"Employee\", first_name || ' ' || last_name))\n ),\n VERSION '1.0',\n STANDALONE YES\n ).getClobVal() AS xml_doc\nFROM employees;\n```\n\n---\n\n## XQuery with XMLQuery and XMLExists\n\n```sql\n-- XMLQuery: execute XQuery expression, return XMLType\nSELECT XMLQuery(\n 'for $o in /OrderSet/Order\n where $o/Status = \"PENDING\"\n return $o/OrderId'\n PASSING order_xml\n RETURNING CONTENT\n ).getStringVal() AS pending_ids\nFROM order_documents;\n\n-- XMLExists: test with XQuery predicate\nSELECT order_id\nFROM order_documents\nWHERE XMLExists(\n 'for $o in /Order/Items/Item\n where $o/Price > 100\n return $o'\n PASSING order_xml\n );\n\n-- XQuery FLWOR expression\nSELECT XMLQuery(\n 'for $i in /Specification/Materials/Material\n order by $i\n return \u003cmat>{$i/text()}\u003c/mat>'\n PASSING spec_xml\n RETURNING CONTENT\n ).getClobVal() AS sorted_materials\nFROM product_specs\nWHERE product_id = 101;\n```\n\n---\n\n## XML Indexes\n\n### XMLIndex (Structured and Unstructured)\n\n```sql\n-- Unstructured XMLIndex: indexes all text nodes and attribute values\nCREATE INDEX idx_contracts_xml\n ON contracts (contract_xml)\n INDEXTYPE IS XDB.XMLIndex;\n\n-- Structured XMLIndex: index specific paths for targeted performance\nCREATE INDEX idx_contracts_structured\n ON contracts (contract_xml)\n INDEXTYPE IS XDB.XMLIndex\n PARAMETERS ('PATHS (\n path (/Contract/ContractId)\n path (/Contract/EffectiveDate)\n path (/Contract/Value)\n )');\n\n-- Drop XML index\nDROP INDEX idx_contracts_xml;\n```\n\n### Function-Based Index for Common XPath\n\n```sql\n-- When you always query a specific scalar path\nCREATE INDEX idx_spec_product_name\n ON product_specs (\n XMLCast(XMLQuery('/Specification/Name/text()'\n PASSING spec_xml RETURNING CONTENT)\n AS VARCHAR2(200))\n );\n\n-- Query using the same expression to leverage the index\nSELECT product_id\nFROM product_specs\nWHERE XMLCast(XMLQuery('/Specification/Name/text()'\n PASSING spec_xml RETURNING CONTENT)\n AS VARCHAR2(200)) = 'Industrial Widget';\n```\n\n---\n\n## Oracle XML DB (XMLDB) Repository\n\nXMLDB includes a hierarchical repository accessible via WebDAV or FTP protocols, allowing XML documents to be stored in a folder-like structure within the database.\n\n```sql\n-- Create a folder in the XMLDB repository\nCALL DBMS_XDB.CREATEFOLDER('/public/contracts');\n\n-- Create/store an XML resource\nDECLARE\n v_result BOOLEAN;\nBEGIN\n v_result := DBMS_XDB.CREATERESOURCE(\n ABSPATH => '/public/contracts/contract_1001.xml',\n DATA => XMLTYPE(\n '\u003cContract>\n \u003cContractId>1001\u003c/ContractId>\n \u003cStatus>Active\u003c/Status>\n \u003c/Contract>'\n )\n );\n IF NOT v_result THEN\n RAISE_APPLICATION_ERROR(-20001, 'Resource already exists');\n END IF;\n COMMIT;\nEND;\n\n-- Query XML files in the repository using RESOURCE_VIEW\nSELECT PATH(1) AS file_path, XMLType(RES) AS resource_xml\nFROM RESOURCE_VIEW\nWHERE UNDER_PATH(RES, '/public/contracts', 1) = 1;\n\n-- Query content from repository documents\nSELECT x.contract_id, x.status\nFROM RESOURCE_VIEW rv,\n XMLTable('/Contract'\n PASSING XMLType(rv.RES)\n COLUMNS\n contract_id NUMBER PATH 'ContractId',\n status VARCHAR2(20) PATH 'Status'\n ) x\nWHERE UNDER_PATH(rv.RES, '/public/contracts', 1) = 1;\n```\n\n---\n\n## XML Type Conversions and Utilities\n\n```sql\n-- Convert XMLType to CLOB\nSELECT spec_xml.getClobVal() AS xml_clob FROM product_specs;\n\n-- Convert XMLType to VARCHAR2 (if small enough)\nSELECT spec_xml.getStringVal() AS xml_string FROM product_specs WHERE product_id = 101;\n\n-- Convert VARCHAR2/CLOB to XMLType\nSELECT XMLType('\u003croot>\u003cvalue>42\u003c/value>\u003c/root>') AS xml_val FROM DUAL;\n\n-- Validate XML against a registered schema\nSELECT spec_xml.isSchemaValid('http://myapp.com/schemas/product.xsd') AS is_valid\nFROM product_specs;\n\n-- Transform with XSLT\nSELECT XMLType('\u003cdata>\u003citem>Hello\u003c/item>\u003c/data>').transform(\n XMLType('\u003c?xml version=\"1.0\"?>\n \u003cxsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\">\n \u003cxsl:template match=\"/\">\n \u003cresult>\u003cxsl:value-of select=\"/data/item\"/>\u003c/result>\n \u003c/xsl:template>\n \u003c/xsl:stylesheet>')\n).getClobVal() AS transformed\nFROM DUAL;\n\n-- Pretty-print XML\nSELECT XMLSerialize(DOCUMENT spec_xml INDENT SIZE = 2) AS formatted_xml\nFROM product_specs;\n```\n\n---\n\n## Best Practices\n\n- **Choose Binary XML storage** (not CLOB, not object-relational) for new XMLType columns unless you have a specific reason for the alternatives. Binary XML offers the best balance of storage efficiency, query performance, and flexibility.\n- **Use XMLTable and XMLQuery** (not deprecated EXTRACTVALUE/EXTRACT) for all new development.\n- **Index specific XPath expressions** with structured XMLIndex or function-based indexes for frequently queried paths.\n- **Avoid fetching entire XML documents to application code** to extract one field. Use XMLTable/XMLQuery to shred at the database level.\n- **Use XMLSerialize** for controlled serialization when generating XML output — it handles encoding, indentation, and namespace declarations correctly.\n- **For large XML documents (>1MB)**, consider CLOB storage and process with `DBMS_XMLPARSER` streaming API rather than loading entire documents into memory.\n- **Validate XML on insert** using schema registration or `IS JSON` equivalent (`XMLTYPE ... VALIDATING`) to catch structure errors early.\n\n---\n\n## Common Mistakes\n\n### Mistake 1: Using Deprecated Functions\n\n```sql\n-- DEPRECATED: avoid in new code\nSELECT EXTRACTVALUE(xml_col, '/Root/Value') FROM t;\nSELECT EXTRACT(xml_col, '/Root/Child') FROM t;\n\n-- PREFERRED\nSELECT XMLCast(XMLQuery('/Root/Value/text()' PASSING xml_col RETURNING CONTENT) AS VARCHAR2(100)) FROM t;\nSELECT XMLQuery('/Root/Child' PASSING xml_col RETURNING CONTENT) FROM t;\n```\n\n### Mistake 2: CLOB Storage for Frequently Queried XML\n\nStoring as CLOB means every XPath query must parse the document from scratch. For XML that is queried often, use Binary XML or Object-Relational with an XMLIndex.\n\n### Mistake 3: No Namespace Handling\n\nForgetting to declare namespaces in XMLTable/XMLQuery causes silent empty result sets — Oracle returns no rows instead of an error when namespace-qualified paths don't match.\n\n```sql\n-- WRONG: ignores namespace, returns nothing\nSELECT * FROM XMLTable('/Order/Id' PASSING namespace_xml_col COLUMNS id NUMBER PATH '.');\n\n-- RIGHT: declare the namespace\nSELECT * FROM XMLTable(\n XMLNAMESPACES('http://orders.com/v1' AS \"o\"),\n '/o:Order/o:Id'\n PASSING namespace_xml_col\n COLUMNS id NUMBER PATH '.'\n);\n```\n\n### Mistake 4: Comparing XMLType with =\n\nXMLType cannot be compared with `=`. Use XMLExists, XMLQuery, or extract a scalar value first.\n\n```sql\n-- WRONG\nWHERE spec_xml = XMLType('\u003cSpecification>...')\n\n-- RIGHT: compare extracted values\nWHERE XMLCast(XMLQuery('/Specification/ProductId/text()'\n PASSING spec_xml RETURNING CONTENT) AS NUMBER) = 101\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 19c XML Developer's Kit Programmer's Guide (ADXDK)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adxdk/)\n- [Oracle Database 19c SQL Language Reference — XML Functions](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/)\n- [Oracle XML DB Developer's Guide (ADXDB)](https://docs.oracle.com/en/database/oracle/oracle-database/19/adxdb/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17255,"content_sha256":"fb1699b7c39d0ed815cc8a60eee74d0e8597d0504a427f31675bc85393c4b07c"},{"filename":"architecture/dataguard.md","content":"# Oracle Data Guard\n\n## Overview\n\nOracle Data Guard is Oracle's high availability, disaster recovery, and data protection solution. It maintains one or more synchronized copies of a production database (the **primary**) called **standby databases**. If the primary becomes unavailable, a standby can be activated to take over, minimizing downtime and data loss.\n\nData Guard is a native feature of Oracle Database **Enterprise Edition**. Standard Edition is not supported. A primary can ship redo directly to up to 30 standby databases; with cascading, configurations can scale beyond 30 total standbys.\n\nThe two core services are:\n- **Redo Transport Services** — automates transfer of redo data from primary to standby destinations; detects and resolves archive log gaps automatically.\n- **Apply Services** — automatically applies received redo on the standby to maintain synchronization.\n\n---\n\n## 1. Standby Database Types\n\n### Physical Standby\n\nA block-for-block identical copy of the primary database. Redo Apply (Media Recovery via the MRP0 process) keeps it synchronized.\n\n- Supports all data types and object types without restriction.\n- Can be opened read-only while redo apply is active (requires Active Data Guard license for real-time query).\n- Physical corruptions on the primary are **not** propagated to the standby.\n- Supports RMAN backup offloading and rolling patch application.\n- Most efficient apply method — bypasses SQL layer entirely.\n\n### Logical Standby\n\nMaintained via **SQL Apply** (LogMiner-based): redo is transformed into SQL statements and executed on an open read-write database.\n\n- Open for read-write during apply; SQL Apply-maintained tables are protected from user DML by logical standby guard rules.\n- Can carry additional indexes, partitioning, and materialized views not on the primary.\n- Enables rolling database software upgrades using the `DBMS_ROLLING` package.\n- Logical standby support has datatype and object restrictions. Starting with Oracle Database 12.2, newer types/features (for example long identifiers) are supported for logical replication by using `DBMS_ROLLING` or Oracle GoldenGate, not generic SQL Apply in all cases.\n\n### Snapshot Standby\n\nA physical standby temporarily converted to read-write for testing or development.\n\n- Receives and archives redo from the primary but does **not** apply it.\n- Local writes are discarded when converting back; Flashback Database is used internally.\n- Cannot cascade to other standby databases.\n\n```sql\n-- Convert physical standby to snapshot standby\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;\nSTARTUP MOUNT;\nALTER DATABASE CONVERT TO SNAPSHOT STANDBY;\nALTER DATABASE OPEN READ WRITE;\n\n-- Convert back to physical standby\nALTER DATABASE CONVERT TO PHYSICAL STANDBY;\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE DISCONNECT FROM SESSION;\n```\n\nVia Broker:\n```\nDGMGRL> CONVERT DATABASE 'standby_db' TO SNAPSHOT STANDBY;\nDGMGRL> CONVERT DATABASE 'standby_db' TO PHYSICAL STANDBY;\n```\n\n### Far Sync Instance\n\nA lightweight remote instance (control file only — no datafiles) that accepts redo from the primary and forwards it to terminal standbys.\n\n- Enables zero data loss at geographically distant standbys without direct synchronous transmission over a long-distance link.\n- Cannot be opened; cannot run redo apply; cannot become primary.\n- Requires Active Data Guard Far Sync license.\n\n```\nDGMGRL> ADD FAR_SYNC farsync_inst AS CONNECT IDENTIFIER IS farsync_tns;\nDGMGRL> EDIT DATABASE primary_db SET PROPERTY\n RedoRoutes='(LOCAL : farsync_inst SYNC)(farsync_inst : standby_db ASYNC)';\nDGMGRL> ENABLE FAR_SYNC farsync_inst;\n```\n\n---\n\n## 2. Protection Modes\n\nProtection modes control the trade-off between data protection and primary database performance.\n\n### Maximum Performance (Default)\n\n- Transactions commit as soon as redo is written to the online redo log.\n- Redo transport to standby is **ASYNC** (no commit wait).\n- Potential data loss equal to the transport lag.\n- `LOG_ARCHIVE_DEST_n` must use: `ASYNC NOAFFIRM`\n\n```sql\nALTER DATABASE SET STANDBY DATABASE TO MAXIMIZE PERFORMANCE;\n```\n\n### Maximum Availability\n\n- Transactions commit after redo is received at the standby and acknowledged.\n- Uses **SYNC** transport.\n- If all synchronized standbys become unreachable, automatically falls back to Maximum Performance (primary stays up).\n- Two sub-variants:\n - `SYNC AFFIRM` — waits for redo written to disk on standby (maximum durability)\n - `SYNC NOAFFIRM` (FastSync) — waits only for redo received in standby memory (better performance; small risk on simultaneous power failures at both sites)\n- Recommended for most mission-critical deployments.\n\n```sql\nALTER DATABASE SET STANDBY DATABASE TO MAXIMIZE AVAILABILITY;\n```\n\n### Maximum Protection\n\n- Uses **SYNC AFFIRM**.\n- Primary **shuts down** if it cannot write to a synchronized standby (does not fall back).\n- Can only be set from Maximum Availability mode with at least one synchronized standby in place.\n- Oracle recommends a minimum of two standby databases to avoid a single standby failure shutting down the primary.\n\n```sql\nALTER DATABASE SET STANDBY DATABASE TO MAXIMIZE PROTECTION;\n```\n\n```sql\n-- Verify current mode\nSELECT PROTECTION_MODE, PROTECTION_LEVEL FROM V$DATABASE;\n```\n\n---\n\n## 3. Redo Transport\n\n### LOG_ARCHIVE_DEST_n\n\nConfigures where redo is archived and shipped. Up to 31 destinations (`LOG_ARCHIVE_DEST_1` through `LOG_ARCHIVE_DEST_31`).\n\n```sql\n-- Typical primary → standby transport (ASYNC)\nALTER SYSTEM SET LOG_ARCHIVE_DEST_2 =\n 'SERVICE=standby_db ASYNC NOAFFIRM\n DB_UNIQUE_NAME=standby_db\n VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE)\n REOPEN=60'\n SCOPE=BOTH;\n\nALTER SYSTEM SET LOG_ARCHIVE_DEST_STATE_2 = ENABLE SCOPE=BOTH;\n\n-- Synchronous transport (SYNC AFFIRM — Maximum Availability)\nALTER SYSTEM SET LOG_ARCHIVE_DEST_2 =\n 'SERVICE=standby_db SYNC AFFIRM NET_TIMEOUT=30\n DB_UNIQUE_NAME=standby_db\n VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE)'\n SCOPE=BOTH;\n```\n\nKey `LOG_ARCHIVE_DEST_n` attributes:\n\n| Attribute | Description |\n|---|---|\n| `SERVICE=\u003cname>` | Oracle Net service name for remote destination |\n| `SYNC` / `ASYNC` | Synchronous or asynchronous transport |\n| `AFFIRM` / `NOAFFIRM` | Wait for redo written to disk at standby (`AFFIRM`) or not |\n| `NET_TIMEOUT=\u003csecs>` | How long LGWR waits for acknowledgment (SYNC only) |\n| `VALID_FOR=(\u003clog_type>,\u003crole>)` | When to send: e.g. `(ONLINE_LOGFILES,PRIMARY_ROLE)` |\n| `DB_UNIQUE_NAME=\u003cname>` | Required when `LOG_ARCHIVE_CONFIG` DG_CONFIG list is set |\n| `REOPEN=\u003csecs>` | Min seconds between reconnect attempts on failure |\n| `COMPRESSION=ENABLE` | Compress redo in transit (requires Advanced Compression license) |\n| `DELAY=\u003cminutes>` | Apply delay on standby (default 30 minutes if value omitted) |\n| `MANDATORY` | Primary stalls if this destination cannot be archived |\n| `ALTERNATE=LOG_ARCHIVE_DEST_n` | Failover destination if this one fails |\n\n### LOG_ARCHIVE_CONFIG\n\nRequired when using `DB_UNIQUE_NAME` in `LOG_ARCHIVE_DEST_n`:\n\n```sql\n-- Set on every member\nALTER SYSTEM SET LOG_ARCHIVE_CONFIG='DG_CONFIG=(primary_db,standby_db)' SCOPE=BOTH;\n```\n\n### Standby Redo Logs\n\nRequired for real-time apply and synchronous transport. Size must be at least as large as the largest online redo log. Group count must be at least (primary online redo groups + 1) per thread.\n\n```sql\n-- Add standby redo log groups (run on standby)\nALTER DATABASE ADD STANDBY LOGFILE THREAD 1 SIZE 500M;\n\n-- View standby redo logs\nSELECT GROUP#, MEMBERS, BYTES/1048576 AS SIZE_MB, STATUS\nFROM V$STANDBY_LOG;\n```\n\n### Monitoring Redo Transport\n\n```sql\n-- Destination status\nSELECT DEST_ID, STATUS, TARGET, ARCHIVED_THREAD#, ARCHIVED_SEQ#\nFROM V$ARCHIVE_DEST_STATUS\nWHERE TARGET = 'STANDBY';\n\n-- Check for archive gaps (run on physical standby)\nSELECT THREAD#, LOW_SEQUENCE#, HIGH_SEQUENCE# FROM V$ARCHIVE_GAP;\n\n-- SYNC response time histogram\nSELECT * FROM V$REDO_DEST_RESP_HISTOGRAM WHERE DEST_ID = 2;\n```\n\n---\n\n## 4. Apply Services\n\n### Redo Apply (Physical Standby)\n\n```sql\n-- Start managed recovery (disconnected background process)\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE DISCONNECT;\n\n-- Stop managed recovery\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;\n\n-- Remove a delay override\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE NODELAY;\n```\n\n> Note: The `USING CURRENT LOGFILE` clause was deprecated in Oracle 12.1 and is no longer required. Real-time apply is enabled automatically when standby redo logs are configured.\n\n### SQL Apply (Logical Standby)\n\n```sql\n-- Start SQL Apply (real-time)\nALTER DATABASE START LOGICAL STANDBY APPLY IMMEDIATE;\n\n-- Stop SQL Apply (waits for in-progress transactions to complete)\nALTER DATABASE STOP LOGICAL STANDBY APPLY;\n```\n\n### Monitoring Apply Progress\n\n```sql\n-- Primary view for process status (replaces deprecated V$MANAGED_STANDBY)\nSELECT ROLE, THREAD#, SEQUENCE#, ACTION\nFROM V$DATAGUARD_PROCESS;\n\n-- Apply and transport lag (run on standby)\nSELECT NAME, VALUE, TIME_COMPUTED, DATUM_TIME\nFROM V$DATAGUARD_STATS\nWHERE NAME IN ('transport lag', 'apply lag', 'apply finish time');\n\n-- Overall database status\nSELECT DATABASE_ROLE, OPEN_MODE, PROTECTION_MODE, SWITCHOVER_STATUS\nFROM V$DATABASE;\n```\n\n---\n\n## 5. Key Initialization Parameters\n\n| Parameter | Description |\n|---|---|\n| `DB_NAME` | Same on primary and all standbys |\n| `DB_UNIQUE_NAME` | Unique identifier per database; preserved across role transitions |\n| `LOG_ARCHIVE_CONFIG` | `DG_CONFIG=(...)` list of all members; required for `DB_UNIQUE_NAME` in dest |\n| `LOG_ARCHIVE_DEST_n` | Archive/redo transport destinations (up to 31) |\n| `LOG_ARCHIVE_DEST_STATE_n` | `ENABLE`, `DEFER`, or `ALTERNATE` |\n| `LOG_ARCHIVE_FORMAT` | Filename format for archived logs |\n| `REMOTE_LOGIN_PASSWORDFILE` | `EXCLUSIVE` or `SHARED` — enables redo transport authentication |\n| `FAL_SERVER` | Oracle Net alias of the server that resolves redo gaps |\n| `DB_FILE_NAME_CONVERT` | Converts primary datafile paths to standby paths |\n| `LOG_FILE_NAME_CONVERT` | Converts primary redo log paths to standby paths |\n| `STANDBY_FILE_MANAGEMENT` | `AUTO` — creates/deletes standby datafiles automatically |\n| `DG_BROKER_START` | `TRUE` — starts the DMON background process |\n| `DG_BROKER_CONFIG_FILE1/2` | Locations of broker configuration files |\n| `DATA_GUARD_SYNC_LATENCY` | Max seconds to wait for secondary SYNC standbys after first acknowledges |\n| `STANDBY_DB_PRESERVE_STATES` | `NONE\\|ALL\\|SESSION\\|BUFFER` — preserve sessions/buffers during role transition (12.2+) |\n| `ENABLED_PDBS_ON_STANDBY` | Replicate a subset of PDBs to standby (CDB only) |\n| `COMPATIBLE` | Must match on all members; logical standby may have a higher setting |\n\n---\n\n## 6. Data Guard Broker (DGMGRL)\n\nBroker automates and centralizes configuration, monitoring, and role transitions. Using Broker is strongly recommended over manual `LOG_ARCHIVE_DEST_n` management.\n\n### Enabling the Broker\n\n```sql\n-- Enable on every managed database instance (primary and all standbys)\nALTER SYSTEM SET DG_BROKER_START = TRUE SCOPE=BOTH;\n```\n\n### Creating a Configuration\n\n```\n$ dgmgrl\n\nDGMGRL> CONNECT SYS@primary_db;\n\nDGMGRL> CREATE CONFIGURATION 'DRSolution' AS\n PRIMARY DATABASE IS 'primary_db'\n CONNECT IDENTIFIER IS primary_tns;\n\nDGMGRL> ADD DATABASE 'standby_db'\n AS CONNECT IDENTIFIER IS standby_tns\n MAINTAINED AS PHYSICAL;\n\nDGMGRL> ENABLE CONFIGURATION;\n```\n\n### Common DGMGRL Commands\n\n```\n-- Health and status\nDGMGRL> SHOW CONFIGURATION;\nDGMGRL> SHOW CONFIGURATION LAG VERBOSE;\nDGMGRL> SHOW DATABASE VERBOSE 'standby_db';\nDGMGRL> SHOW DATABASE 'standby_db' 'ApplyLag';\nDGMGRL> SHOW DATABASE 'standby_db' 'TransportLag';\n\n-- Validation\nDGMGRL> VALIDATE DATABASE 'standby_db';\nDGMGRL> VALIDATE DATABASE STRICT 'standby_db';\nDGMGRL> VALIDATE NETWORK CONFIGURATION FOR ALL;\n\n-- Edit properties\nDGMGRL> EDIT DATABASE 'standby_db' SET PROPERTY LogXptMode='SYNC';\nDGMGRL> EDIT CONFIGURATION SET PROTECTION MODE AS MaxAvailability;\n\n-- Role transitions\nDGMGRL> SWITCHOVER TO 'standby_db';\nDGMGRL> FAILOVER TO 'standby_db';\nDGMGRL> REINSTATE DATABASE 'old_primary_db';\n\n-- Maintenance\nDGMGRL> DISABLE CONFIGURATION;\nDGMGRL> REMOVE DATABASE 'standby_db';\nDGMGRL> EXPORT CONFIGURATION TO '/path/config.dat';\nDGMGRL> IMPORT CONFIGURATION FROM '/path/config.dat';\n```\n\n---\n\n## 7. Switchover and Failover\n\n### Switchover\n\nA planned, graceful role reversal — both databases remain intact, no data loss. Used for planned maintenance, patching, or testing.\n\n```\n-- Verify readiness before switchover\nDGMGRL> VALIDATE DATABASE 'standby_db';\n\n-- Perform switchover (Broker manages both sides)\nDGMGRL> SWITCHOVER TO 'standby_db';\n\n-- Verify new roles\nDGMGRL> SHOW CONFIGURATION;\n```\n\nManual switchover (without Broker):\n```sql\n-- On primary: verify, then initiate\nALTER DATABASE SWITCHOVER TO standby_db VERIFY;\nALTER DATABASE SWITCHOVER TO standby_db;\nALTER DATABASE OPEN;\n\n-- On new standby (former primary)\nSTARTUP MOUNT;\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE DISCONNECT FROM SESSION;\n```\n\n### Failover\n\nAn emergency operation when the primary is unavailable and cannot be recovered quickly. May involve data loss unless all redo was received.\n\n```\n-- Failover via Broker\nDGMGRL> FAILOVER TO 'standby_db';\n\n-- Skip unapplied redo (accepts potential data loss)\nDGMGRL> FAILOVER TO 'standby_db' IMMEDIATE;\n```\n\nManual failover (without Broker):\n```sql\n-- If primary is mountable, flush remaining redo first\nALTER SYSTEM FLUSH REDO TO standby_db;\n\n-- Check for gaps on standby\nSELECT THREAD#, LOW_SEQUENCE#, HIGH_SEQUENCE# FROM V$ARCHIVE_GAP;\n\n-- Cancel apply and activate\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE CANCEL;\nALTER DATABASE FAILOVER TO standby_db;\n-- Last resort (accepts data loss):\nALTER DATABASE ACTIVATE PHYSICAL STANDBY DATABASE;\nALTER DATABASE OPEN;\n```\n\n### Reinstating the Old Primary\n\nAfter failover, Flashback Database is used to roll the old primary back and re-synchronize it as a standby:\n\n```\nDGMGRL> REINSTATE DATABASE 'old_primary_db';\n```\n\n> Flashback Database must be enabled **before** the failover occurs:\n> ```sql\n> ALTER DATABASE FLASHBACK ON;\n> ```\n\n---\n\n## 8. Fast-Start Failover (FSFO)\n\nFSFO lets an observer process automatically initiate failover when the primary is unavailable, without DBA intervention. Requires Broker.\n\n```\n-- Enable FSFO (target must be a synchronized standby)\nDGMGRL> ENABLE FAST_START FAILOVER;\n\n-- Start the observer process\nDGMGRL> START OBSERVER;\n\n-- Validate the configuration\nDGMGRL> VALIDATE FAST_START FAILOVER;\n\n-- Test mode only (no automatic failover)\nDGMGRL> ENABLE FAST_START FAILOVER OBSERVE ONLY;\n```\n\nKey FSFO requirements:\n- Flashback Database must be enabled on both primary and standby.\n- Standby redo logs must be configured on both databases.\n- Observer must be running; if not: `ORA-16819` is raised.\n- Up to 4 observers supported (26ai).\n\n```sql\n-- FSFO status from SQL\nSELECT FS_FAILOVER_STATUS, FS_FAILOVER_CURRENT_TARGET,\n FS_FAILOVER_THRESHOLD, FS_FAILOVER_OBSERVER_PRESENT\nFROM V$DATABASE;\n```\n\n---\n\n## 9. Active Data Guard (Read Offload)\n\nActive Data Guard allows a physical standby to be open **read-only** while simultaneously applying redo. Requires a separate Active Data Guard license.\n\n```sql\n-- Open the standby read-only, then start/ensure Redo Apply\nALTER DATABASE OPEN READ ONLY;\nALTER DATABASE RECOVER MANAGED STANDBY DATABASE DISCONNECT;\n\n-- Confirm the state\nSELECT OPEN_MODE, DB_UNIQUE_NAME FROM V$DATABASE;\n-- Expect: READ ONLY WITH APPLY\n```\n\nSession-level lag tolerance (Active Data Guard):\n\n```sql\n-- Reject query if standby lag exceeds 2 seconds\nALTER SESSION SET STANDBY_MAX_DATA_DELAY = 2;\n\n-- Block until all received redo is applied\nALTER SESSION SYNC WITH PRIMARY;\n```\n\n---\n\n## 10. Monitoring Views Reference\n\n| View | Description |\n|---|---|\n| `V$DATABASE` | Role, protection mode, open mode, switchover status, FSFO status |\n| `V$DATAGUARD_PROCESS` | All Data Guard background processes with role, thread, sequence, and action |\n| `V$DATAGUARD_STATS` | Transport lag, apply lag, apply finish time |\n| `V$ARCHIVE_DEST` | Configuration and status of all archive destinations |\n| `V$ARCHIVE_DEST_STATUS` | Runtime status including `GAP_STATUS` per destination |\n| `V$ARCHIVE_GAP` | Detected archive gaps on physical standby |\n| `V$STANDBY_LOG` | Standby redo log groups |\n| `V$DATAGUARD_STATUS` | Messages written to the alert log by Data Guard |\n| `V$REDO_DEST_RESP_HISTOGRAM` | SYNC transport response time distribution |\n| `V$STANDBY_EVENT_HISTOGRAM` | Apply lag histogram |\n| `V$FS_FAILOVER_OBSERVERS` | Observer information for FSFO |\n| `V$MANAGED_STANDBY` | Deprecated since 12.2 — use `V$DATAGUARD_PROCESS` |\n\nBroker-specific views (26ai):\n\n| View | Description |\n|---|---|\n| `V$DG_BROKER_CONFIG` | Broker configuration properties |\n| `V$DG_BROKER_PROPERTY` | Detailed broker properties (26ai) |\n| `V$DG_BROKER_ROLE_CHANGE` | Last 10 recorded role changes (26ai) |\n| `V$FAST_START_FAILOVER_CONFIG` | FSFO configuration statistics (26ai) |\n| `V$FS_LAG_HISTOGRAM` | Failover lag statistics (26ai) |\n\n---\n\n## 11. Best Practices\n\n- **Use Data Guard Broker (DGMGRL)** for all operations. Manual `LOG_ARCHIVE_DEST_n` management is error-prone and difficult to maintain consistently.\n- **Configure Standby Redo Logs** on both primary and standby. They are required for real-time apply and synchronous transport. Size groups identically to the primary online redo logs.\n- **Use Maximum Availability** with SYNC transport for OLTP workloads where network round-trip to the standby is acceptable (typically \u003c 5ms).\n- **Enable Flashback Database** on the primary before any production use — it is required for reinstatement after failover.\n- **Test switchovers regularly** (quarterly minimum). An untested failover procedure is an assumption, not a plan.\n- **Monitor lag actively.** A standby with 4 hours of apply lag gives 4 hours of recovery time, not instant failover capability.\n- **Back up from the standby** with RMAN to offload backup I/O from the primary.\n- **Set `STANDBY_FILE_MANAGEMENT=AUTO`** to avoid manual datafile management when the primary adds new datafiles.\n\n---\n\n## 12. Common Mistakes\n\n**Not configuring Standby Redo Logs**\nWithout SRLs, real-time apply is not possible and synchronous transport cannot acknowledge. Apply lag increases unnecessarily.\n\n**Incorrect VALID_FOR on LOG_ARCHIVE_DEST_n**\nThe log type and role must match the sending database's state. Misconfiguration causes redo transport to silently stop.\n\n```sql\n-- Correct: ship online logs when acting as primary\nALTER SYSTEM SET LOG_ARCHIVE_DEST_2 =\n 'SERVICE=standby_db ASYNC NOAFFIRM\n DB_UNIQUE_NAME=standby_db\n VALID_FOR=(ONLINE_LOGFILES,PRIMARY_ROLE)';\n```\n\n**Missing DB_UNIQUE_NAME on all members**\nAll databases must have unique `DB_UNIQUE_NAME` values. They can share the same `DB_NAME`.\n\n```sql\nSELECT DB_UNIQUE_NAME, DB_NAME FROM V$DATABASE;\n```\n\n**Failing over without checking for gaps**\nAlways verify gap status before manual failover:\n```sql\nSELECT * FROM V$ARCHIVE_GAP;\n```\n\n**Not enabling Flashback Database before failover**\nWithout Flashback, reinstating the old primary requires a full rebuild from backup.\n\n**Using deprecated syntax**\nThe `USING CURRENT LOGFILE` clause (deprecated 12.1) and `V$MANAGED_STANDBY` (deprecated 12.2) still function but should not be used in new scripts. Use `V$DATAGUARD_PROCESS` instead.\n\n---\n\n## 13. Oracle Version Notes (19c vs 26ai)\n\n- **Oracle 19c:** Full Data Guard functionality — physical, logical, and snapshot standbys; Far Sync; all three protection modes; Broker; DGMGRL; FSFO. `V$MANAGED_STANDBY` is the primary process monitoring view (deprecated in 12.2 but still present).\n- **Oracle 21c:** Introduced Data Guard for Pluggable Databases (DG PDB) — protect individual PDBs within a CDB without replicating the entire container. PDB-level switchover/failover via Broker only.\n- **Oracle 23ai:** `DBMS_ROLLING` available in multitenant environments.\n- **Oracle 26ai new features:**\n - **`DBMS_DG` PL/SQL API** — programmatic interface for creating and managing broker configurations; tagging support.\n - **Up to 4 observers** for Fast-Start Failover (previously fewer).\n - **Preferred observer affinity** — affinitize observers to the current primary site.\n - **New FSFO properties:** `FastStartFailoverLagType`, `FastStartFailoverLagGraceTime`.\n - **New DGMGRL commands:** `EDIT ALL MEMBERS SET/RESET PROPERTY/PARAMETER`, tag management (`SET TAG`), `SHOW ALL MEMBERS`, `VALIDATE DATABASE STRICT`, `VALIDATE DGConnectIdentifier`, `PREPARE DATABASE FOR DATA GUARD`.\n - **Configuration and member tagging** for organizational labeling.\n - **Automatic temporary file creation** on standby databases.\n - **SQLcl integration:** DGMGRL commands available via Oracle SQLcl.\n - **New monitoring views:** `V$DG_BROKER_PROPERTY`, `V$DG_BROKER_ROLE_CHANGE`, `V$DG_BROKER_TAG`, `V$FAST_START_FAILOVER_CONFIG`, `V$FS_LAG_HISTOGRAM`.\n - **DG PDB enhancements:** DBCA PDB operations in DG environment; GoldenGate Per-PDB Capture as source PDB; automatic source switching on role transition.\n- **`COMPATIBLE` parameter:** Must be identical on all members except logical standbys, which can have a higher setting.\n- **`USING CURRENT LOGFILE`:** Deprecated since 12.1 — remove from any scripts.\n- **`V$MANAGED_STANDBY`:** Deprecated since 12.2 — replaced by `V$DATAGUARD_PROCESS`.\n\n---\n\n## 14. Sources\n\n- [Oracle Data Guard Concepts and Administration, Release 26.1](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/index.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Introduction](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/introduction-to-oracle-data-guard-concepts.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Protection Modes](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/oracle-data-guard-protection-modes.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Redo Transport Services](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/oracle-data-guard-redo-transport-services.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Redo Apply Services](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/oracle-data-guard-redo-apply-services.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Managing Role Transitions](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/managing-oracle-data-guard-role-transitions.html)\n- [Oracle Data Guard Concepts and Administration 26.1 — Creating a Physical Standby](https://docs.oracle.com/en/database/oracle/oracle-database/26/sbydb/creating-oracle-data-guard-physical-standby.html)\n- [Oracle Data Guard Broker, Release 26.1](https://docs.oracle.com/en/database/oracle/oracle-database/26/dgbkr/index.html)\n- [Oracle Data Guard Concepts and Administration 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sbydb/index.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":23059,"content_sha256":"551519a3213f7de1f5c9d0f083cd8d474e0632848c0571628d1fe218e1eb72cf"},{"filename":"architecture/exadata-features.md","content":"# Oracle Exadata Features: Smart Scan, HCC, and Storage Offload\n\n## Overview\n\nOracle Exadata is an engineered system that co-locates Oracle Database servers with intelligent storage cells running Exadata Storage Server Software (CELLSRV). The fundamental design principle is to move SQL processing — filtering, joining, decompression — down to the storage layer, dramatically reducing the amount of data that must traverse the storage network to the database servers.\n\nExadata is available as a physical on-premises system (X9M, X10M), as Exadata Cloud Service (ExaCS) on OCI, and as Exadata Cloud at Customer (ExaDB-C@C). The Exadata-specific features described in this guide are available across all deployment models.\n\n---\n\n## 1. Smart Scan (Cell Offload Processing)\n\nSmart Scan is the core Exadata capability. When Oracle determines that a query can benefit from cell-side processing, it sends the predicate, projection, and optional join filter information to each storage cell. Each cell evaluates the data on its local disk and returns only the rows that satisfy the predicates. This is called **cell offload**.\n\n### How Smart Scan Works\n\nWithout Smart Scan:\n1. Storage cell reads all blocks from disk\n2. All blocks transfer across InfiniBand to DB server\n3. DB server evaluates WHERE clause and discards most rows\n4. DB server returns small result set to the session\n\nWith Smart Scan:\n1. Storage cell reads all blocks from disk\n2. Cell evaluates WHERE clause, projection, and bloom filter in CELLSRV process\n3. Only matching rows (or row pieces) transfer across InfiniBand\n4. DB server performs any remaining aggregation\n\nThe data reduction ratio (bytes sent vs. bytes on disk) is called the **cell offload efficiency**. A 90% offload efficiency means 90% of the data never left the storage layer.\n\n### Prerequisites for Smart Scan\n\nSmart Scan activates only when all of the following conditions are met:\n\n1. **Full segment scan**: The operation is a full table scan or full index fast scan (not a lookup by rowid or index range scan on a selective range).\n2. **Direct-path read**: The operation bypasses the buffer cache and reads directly from storage. This occurs when the segment is large enough to trigger direct reads, or when the session uses `/*+ PARALLEL */` or `/*+ FULL */`.\n3. **Exadata storage**: The segment's datafiles reside on ASM disk groups backed by Exadata storage cells.\n\n```sql\n-- Verify Smart Scan eligibility for a table\nSELECT table_name, blocks, num_rows,\n ROUND(blocks * 8192 / 1024 / 1024, 1) AS size_mb\nFROM user_tables\nWHERE table_name = 'SALES';\n\n-- A table must be large enough to trigger direct-path reads\n-- The threshold is roughly: segment_size > _small_table_threshold * db_block_size\nSELECT name, value\nFROM v$parameter\nWHERE name = '_small_table_threshold';\n\n-- Force Smart Scan for testing (do not use in production)\nALTER SESSION SET \"_serial_direct_read\" = always;\n```\n\n### Monitoring Smart Scan Offload\n\n```sql\n-- System-level Smart Scan statistics\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'cell physical IO interconnect bytes',\n 'cell physical IO interconnect bytes returned by smart scan',\n 'cell scans',\n 'cell blocks processed by cache layer',\n 'cell blocks processed by data layer',\n 'cell blocks processed by txn layer'\n)\nORDER BY name;\n\n-- Calculate offload efficiency (percentage of bytes filtered out)\nSELECT ROUND(\n (1 - (bytes_returned / NULLIF(bytes_read, 0))) * 100,\n 2\n) AS offload_efficiency_pct\nFROM (\n SELECT\n SUM(CASE WHEN name = 'cell physical IO interconnect bytes'\n THEN value ELSE 0 END) AS bytes_returned,\n SUM(CASE WHEN name = 'cell physical IO interconnect bytes returned by smart scan'\n THEN value ELSE 0 END) AS bytes_scan_returned\n FROM v$sysstat\n WHERE name IN (\n 'cell physical IO interconnect bytes',\n 'cell physical IO interconnect bytes returned by smart scan'\n )\n);\n\n-- Session-level: how much was offloaded in the current session\nSELECT name, value\nFROM v$mystat m JOIN v$statname n ON m.statistic# = n.statistic#\nWHERE n.name LIKE 'cell%'\nORDER BY value DESC;\n\n-- SQL-level: check cell_offload_efficiency from AWR\nSELECT sql_id,\n ROUND(AVG(cell_offload_efficiency), 2) AS avg_offload_pct,\n SUM(executions) AS total_execs,\n ROUND(SUM(elapsed_time) / 1e6, 1) AS total_elapsed_sec\nFROM dba_hist_sqlstat\nWHERE cell_offload_efficiency > 0\n AND snap_id >= (SELECT MAX(snap_id) - 24 FROM dba_hist_snapshot)\nGROUP BY sql_id\nORDER BY total_elapsed_sec DESC\nFETCH FIRST 20 ROWS ONLY;\n```\n\n---\n\n## 2. Storage Indexes\n\nStorage Indexes are a purely in-memory optimization maintained automatically by CELLSRV. For each 1 MB region of disk, the storage index tracks the minimum and maximum value for up to 8 columns. When a query has a WHERE clause predicate, the storage cell checks whether the predicate can possibly match any row in each 1 MB region. If not, the entire region is skipped without any disk I/O.\n\nStorage Indexes are:\n- **Automatic**: no DDL or configuration required\n- **In-memory only**: stored in storage cell memory (DRAM), not on disk\n- **Lost on cell restart**: rebuilt over time as data is accessed\n\n### When Storage Indexes Are Most Effective\n\nStorage Indexes are most effective when:\n- Data has **natural clustering** by the predicate column (e.g., `ORDER_DATE` in an orders table inserted in chronological order)\n- Queries filter on **high-cardinality range predicates** (`WHERE order_date BETWEEN ... AND ...`)\n- The table is **not randomly distributed** across the storage cells\n\n```sql\n-- Storage Index statistics (per storage cell, from CellCLI or V$ views)\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'cell blocks helped by minmax predicate', -- blocks eliminated by storage index\n 'cell blocks helped by bloom filter' -- blocks eliminated by bloom filter join\n)\nORDER BY name;\n\n-- Identify queries that would benefit from storage index clustering\n-- Look for full scans with selective date/numeric predicates\nSELECT sql_id, sql_text, executions,\n rows_processed / NULLIF(executions, 0) AS avg_rows_per_exec,\n buffer_gets / NULLIF(executions, 0) AS avg_bufgets_per_exec\nFROM v$sql\nWHERE sql_text LIKE '%ORDER_DATE%'\n AND operation_type = 'TABLE ACCESS FULL'\n AND executions > 10\nORDER BY buffer_gets DESC\nFETCH FIRST 10 ROWS ONLY;\n```\n\n---\n\n## 3. Hybrid Columnar Compression (HCC)\n\nHCC is an Exadata-specific compression technology (also available on OCI Object Storage and ZFS Storage Appliance) that groups column values from multiple rows into **compression units** and compresses them together. Because column data tends to have low cardinality (many repeated values), HCC achieves compression ratios typically 10x–50x better than row-based compression.\n\n### HCC Compression Levels\n\n| Compression Level | Target Use Case | Typical Ratio | Query Overhead |\n|---|---|---|---|\n| `QUERY LOW` | Active query tables | 6x–10x | Low |\n| `QUERY HIGH` | Active query tables | 10x–15x | Low-Medium |\n| `ARCHIVE LOW` | Infrequently queried archives | 15x–25x | Medium |\n| `ARCHIVE HIGH` | Cold archives | 25x–50x | Higher |\n\n`QUERY LOW` and `QUERY HIGH` are suitable for tables queried regularly. `ARCHIVE` levels are for data that is loaded once and rarely queried.\n\n### Applying HCC\n\n```sql\n-- Create a table with HCC compression\nCREATE TABLE sales_archive (\n sale_id NUMBER NOT NULL,\n sale_date DATE NOT NULL,\n product_id NUMBER NOT NULL,\n customer_id NUMBER NOT NULL,\n region_id NUMBER(2) NOT NULL,\n amount NUMBER(12,2) NOT NULL\n)\nCOMPRESS FOR QUERY HIGH;\n\n-- Alter an existing table to use HCC\n-- Note: this only affects NEW data loaded after the ALTER\nALTER TABLE sales_history COMPRESS FOR QUERY HIGH;\n\n-- To recompress existing rows, move the table\nALTER TABLE sales_history MOVE COMPRESS FOR QUERY HIGH ONLINE;\n\n-- Or use CTAS (Create Table As Select) to recompress fully\nCREATE TABLE sales_history_new\nCOMPRESS FOR QUERY HIGH\nAS SELECT * FROM sales_history;\n\n-- Check compression on individual table partitions\nSELECT table_name, partition_name, compression, compress_for,\n blocks, num_rows\nFROM user_tab_partitions\nWHERE table_name = 'SALES_HISTORY'\nORDER BY partition_position;\n```\n\n### HCC and DML\n\nHCC imposes important restrictions on DML operations:\n\n- **INSERT ... AS SELECT (direct path)**: Fully supported; creates HCC compression units\n- **INSERT (conventional)**: Rows are stored in a small row-format area (OLTP compression), not HCC\n- **UPDATE**: Updated rows are migrated out of the HCC compression unit into OLTP format (row migration)\n- **DELETE**: Deleted rows leave \"holes\" in compression units; does not cause migration but wastes space\n\nFor this reason, HCC is appropriate for append-only or archive tables. Mixed read/write workloads should use Advanced Row Compression instead.\n\n```sql\n-- Verify HCC compression units for a table (requires SYSDBA or specific privileges)\nSELECT table_name, compression, compress_for,\n ROUND(blocks * 8192 / 1024 / 1024, 1) AS size_mb,\n num_rows\nFROM dba_tables\nWHERE table_name = 'SALES_ARCHIVE';\n\n-- Check for row migration caused by updates in HCC tables\nSELECT table_name, chain_cnt, num_rows,\n ROUND(chain_cnt / NULLIF(num_rows, 0) * 100, 2) AS migration_pct\nFROM dba_tables\nWHERE compress_for IN ('QUERY LOW', 'QUERY HIGH', 'ARCHIVE LOW', 'ARCHIVE HIGH')\n AND chain_cnt > 0;\n```\n\n---\n\n## 4. I/O Resource Manager (IORM)\n\nIORM is a storage-cell-side resource manager that controls the I/O bandwidth allocation among databases, services, and consumer groups sharing the Exadata infrastructure. Unlike CPU-based DBRM (which runs on the DB server), IORM enforces I/O limits at the physical disk level inside CELLSRV.\n\n### IORM Plans\n\nIORM plans are configured at three levels:\n1. **Inter-database plan**: Allocates I/O shares across different databases on the same Exadata\n2. **Intra-database plan**: Allocates I/O shares across consumer groups within a single database\n3. **Database plan with limits**: Caps the I/O bandwidth a specific database can consume\n\n```sql\n-- Check current IORM settings (from CellCLI on storage cells)\n-- CellCLI> LIST IORMPLAN DETAIL\n\n-- Configure IORM from SQL*Plus (requires SYSDBA, 12c+)\nBEGIN\n DBMS_RESOURCE_MANAGER.CREATE_PENDING_AREA();\n\n -- Create a CDB plan for Exadata resource management\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN(\n plan => 'EXADATA_IO_PLAN',\n comment => 'Exadata I/O resource plan'\n );\n\n -- Assign shares to PDBs; shares and utilization_limit are used by Exadata IORM\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN_DIRECTIVE(\n plan => 'EXADATA_IO_PLAN',\n pluggable_database => 'OLTP_PDB',\n shares => 4,\n utilization_limit => 80\n );\n\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN_DIRECTIVE(\n plan => 'EXADATA_IO_PLAN',\n pluggable_database => 'BATCH_PDB',\n shares => 2,\n utilization_limit => 40\n );\n\n DBMS_RESOURCE_MANAGER.VALIDATE_PENDING_AREA();\n DBMS_RESOURCE_MANAGER.SUBMIT_PENDING_AREA();\nEND;\n/\n\n-- Activate the plan\nALTER SYSTEM SET RESOURCE_MANAGER_PLAN = 'EXADATA_IO_PLAN' SCOPE = BOTH;\n\n-- Verify the active resource plan\nSHOW PARAMETER resource_manager_plan;\n```\n\n---\n\n## 5. Exadata-Specific SQL Hints\n\nSeveral SQL hints interact directly with Exadata offload capabilities.\n\n### Key Hints\n\n| Hint | Effect |\n|---|---|\n| `/*+ FULL(table) */` | Forces a full table scan, enabling Smart Scan |\n| `/*+ PARALLEL(table, degree) */` | Enables parallel query, which forces direct-path reads and Smart Scan |\n| `/*+ NO_PARALLEL(table) */` | Disables parallel query for the table |\n| `/*+ CELL_FLASH_CACHE(KEEP) */` | Pins the object in Exadata Smart Flash Cache |\n| `/*+ CELL_FLASH_CACHE(NONE) */` | Prevents the object from consuming Smart Flash Cache |\n| `/*+ RESULT_CACHE */` | Caches results in the SQL result cache (reduces repeated Smart Scan overhead) |\n| `/*+ CACHE(table) */` | Caches blocks in the database buffer cache (disables direct-path) |\n| `/*+ NO_CACHE(table) */` | Prevents buffer cache caching (maintains direct-path and Smart Scan) |\n| `/*+ VECTOR_TRANSFORM */` | Enables vector transformation for In-Memory aggregation (complements Smart Scan) |\n\n```sql\n-- Force Smart Scan with a parallel hint\nSELECT /*+ FULL(s) PARALLEL(s, 8) */\n region_id,\n COUNT(*),\n SUM(amount)\nFROM sales s\nWHERE sale_date >= DATE '2025-01-01'\n AND sale_date \u003c DATE '2026-01-01'\nGROUP BY region_id;\n\n-- Pin a hot lookup table in Smart Flash Cache to prevent it from\n-- being evicted by large Smart Scan operations\nALTER TABLE country_codes STORAGE (CELL_FLASH_CACHE KEEP);\n\n-- Prevent a very large cold table from polluting the Smart Flash Cache\nALTER TABLE sales_archive_2010 STORAGE (CELL_FLASH_CACHE NONE);\n\n-- Check Smart Flash Cache efficiency\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'cell flash cache read hits',\n 'physical read requests optimized'\n)\nORDER BY name;\n```\n\n---\n\n## 6. Monitoring Exadata Offload Efficiency\n\n### Key V$ Views for Exadata Monitoring\n\n```sql\n-- Overall cell offload efficiency by SQL statement (current)\nSELECT sql_id,\n child_number,\n io_cell_offload_eligible_bytes / 1024 / 1024 AS eligible_mb,\n io_cell_offload_returned_bytes / 1024 / 1024 AS returned_mb,\n ROUND(\n (1 - io_cell_offload_returned_bytes /\n NULLIF(io_cell_offload_eligible_bytes, 0)) * 100,\n 2\n ) AS offload_efficiency_pct,\n io_cell_uncompressed_bytes / 1024 / 1024 AS uncompressed_mb\nFROM v$sql\nWHERE io_cell_offload_eligible_bytes > 0\nORDER BY io_cell_offload_eligible_bytes DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- Historical offload efficiency from AWR\nSELECT snap_id,\n sql_id,\n ROUND(AVG(cell_offload_efficiency), 2) AS avg_offload_pct,\n SUM(io_cell_offload_eligible_bytes) / 1024 / 1024 / 1024 AS eligible_gb,\n SUM(io_cell_offload_returned_bytes) / 1024 / 1024 / 1024 AS returned_gb\nFROM dba_hist_sqlstat\nWHERE cell_offload_efficiency > 0\n AND snap_id >= (SELECT MAX(snap_id) - 24 FROM dba_hist_snapshot)\nGROUP BY snap_id, sql_id\nORDER BY eligible_gb DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- Cell metric statistics (Exadata storage cell performance)\nSELECT metric_name,\n AVG(average_value) AS avg_value,\n MAX(maximum_value) AS max_value,\n MIN(minimum_value) AS min_value\nFROM v$cell_metric_desc cmd\nJOIN v$cell_metric cm ON cmd.metric_id = cm.metric_id\nGROUP BY metric_name\nORDER BY metric_name;\n\n-- Check Smart Flash Cache hit rate\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'cell flash cache read hits',\n 'physical read total IO requests',\n 'physical read requests optimized'\n)\nORDER BY name;\n\n-- Compute Smart Flash Cache hit %\nSELECT ROUND(\n SUM(CASE WHEN name = 'cell flash cache read hits' THEN value END) /\n NULLIF(SUM(CASE WHEN name = 'physical read total IO requests' THEN value END), 0) * 100,\n 2\n) AS flash_cache_hit_pct\nFROM v$sysstat\nWHERE name IN ('cell flash cache read hits', 'physical read total IO requests');\n```\n\n### EXPLAIN PLAN and Exadata Offload Indication\n\n```sql\n-- Check whether the optimizer plans to use cell offload\nEXPLAIN PLAN FOR\nSELECT region_id, COUNT(*), SUM(amount)\nFROM sales\nWHERE sale_date >= DATE '2025-01-01'\nGROUP BY region_id;\n\n-- Look for \"TABLE ACCESS FULL\" with \"Batched Disk Reads\" -- indicates direct path\n-- The key phrase in the plan is \"Batched Disk Reads\" or \"storage\" predicate offload\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(FORMAT => '+PREDICATE'));\n\n-- Use cell_offload_plan_display to show offload predicates in execution plan\nALTER SESSION SET \"_cell_offload_plan_display\" = ALWAYS;\n\nEXPLAIN PLAN FOR\nSELECT region_id, SUM(amount)\nFROM sales\nWHERE sale_date >= DATE '2025-01-01'\n AND amount > 1000\nGROUP BY region_id;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Plan will show \"Storage Filter\" and \"Storage Project\" annotations\n-- indicating which predicates and columns are offloaded to the cell\n```\n\n---\n\n## 7. Cell Offload Metrics Reference\n\n| Statistic Name | Description |\n|---|---|\n| `cell physical IO interconnect bytes` | Total bytes sent from storage cells to DB servers |\n| `cell physical IO interconnect bytes returned by smart scan` | Bytes returned specifically by Smart Scan operations |\n| `cell scans` | Number of Smart Scan operations initiated |\n| `cell blocks processed by cache layer` | Blocks processed by the cell caching layer |\n| `cell blocks helped by minmax predicate` | Blocks eliminated by Storage Indexes |\n| `cell blocks helped by bloom filter` | Blocks eliminated by Bloom filter join offload |\n| `cell flash cache read hits` | Reads served from Smart Flash Cache without HDD access |\n| `physical read requests optimized` | Physical reads served by Smart Flash Cache |\n| `IO megabytes read total` | Total MB read from Exadata storage cells |\n| `IO megabytes written total` | Total MB written to Exadata storage cells |\n\n---\n\n## 8. Best Practices\n\n- **Load data into HCC tables using direct-path INSERT.** Conventional insert defeats HCC by writing rows in row format. Always use `INSERT /*+ APPEND */` or `INSERT ... AS SELECT` for large loads.\n- **Size the private InfiniBand network appropriately.** The Exadata InfiniBand (or RoCE in X9M) network carries both Smart Scan results and Cache Fusion traffic (in RAC). Monitor for saturation with `V$CELL_METRIC`.\n- **Use IORM to protect OLTP latency from large analytics queries.** Without IORM, a parallel Smart Scan consuming all cell I/O bandwidth causes OLTP queries to queue behind it.\n- **Keep statistics current on Exadata.** The CBO must know that a full table scan is appropriate to generate a plan that triggers Smart Scan. Stale statistics can cause the optimizer to choose index lookups that bypass Smart Scan.\n- **Do not force index usage on large Exadata tables.** Index range scans use small I/Os and bypass Smart Scan. On Exadata, full scans with Smart Scan often outperform selective index scans for range predicates touching more than 5–10% of rows.\n- **Use `QUERY HIGH` for reporting tables loaded daily and `ARCHIVE HIGH` only for data that will never be updated.** Mixing ARCHIVE compression with UPDATE-heavy workflows causes severe row migration and performance degradation.\n\n---\n\n## 9. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Expecting Smart Scan on Small Tables\n\nSmart Scan only activates for large segments using direct-path reads. If a table fits in the buffer cache, Oracle will use cached reads and Smart Scan will not activate. The threshold is governed by `_small_table_threshold`.\n\n```sql\n-- Check threshold (in database blocks)\nSELECT name, value FROM v$parameter WHERE name = '_small_table_threshold';\n-- Multiply by block_size to get byte threshold\n-- Tables below this size will use buffer cache reads, not Smart Scan\n```\n\n### Mistake 2: Using HCC on OLTP Tables\n\nHCC is fundamentally incompatible with frequent DML. Each UPDATE on an HCC row causes the row to migrate out of the compression unit into OLTP format. Over time, the table becomes a mixture of compressed and uncompressed rows, wasting space and slowing reads.\n\n**Fix:** Use `COMPRESS FOR OLTP` (Advanced Row Compression) for OLTP tables. Reserve HCC for append-only or archive tables.\n\n### Mistake 3: Disabling Cell Offload System-Wide\n\nSetting `cell_offload_processing = FALSE` at the system level disables Smart Scan globally. This is sometimes done as a workaround for a specific query bug, but the parameter is then forgotten, leaving the Exadata running as a normal database server.\n\n```sql\n-- Check if cell offload is enabled\nSELECT name, value FROM v$parameter WHERE name = 'cell_offload_processing';\n-- Value should be TRUE for Exadata\n\n-- If set to FALSE for a session-level workaround, never persist it system-wide\n-- ALTER SESSION SET cell_offload_processing = FALSE; -- session only, acceptable\n-- ALTER SYSTEM SET cell_offload_processing = FALSE; -- NEVER do this\n```\n\n### Mistake 4: Ignoring Storage Index Invalidation After Bulk Loads\n\nStorage Indexes are rebuilt in memory as data is accessed. After a large bulk load or partition exchange, the Storage Index for the affected regions is cold. The first few executions of queries against newly loaded data will not benefit from Storage Index skipping. This is expected behavior, not a performance regression.\n\n### Mistake 5: Running Exadata Diagnostics Only at the Database Layer\n\nExadata performance problems often originate in the storage cells (CELLSRV process, Flash Cache, disk I/O). Cell-level diagnostics require `CellCLI` access or Exadata Metrics available through OCI console. Always check cell-level metrics alongside `V$SYSSTAT` before concluding a performance issue is query-related.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Exadata Database Machine System Overview](https://docs.oracle.com/en/engineered-systems/exadata-database-machine/) — Smart Scan, Storage Indexes, HCC, IORM\n- [Oracle Database SQL Language Reference 19c — COMPRESS clause](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html) — HCC compression levels\n- [DBMS_RESOURCE_MANAGER (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_RESOURCE_MANAGER.html) — IORM plan directives\n- [Oracle Exadata System Software User's Guide](https://docs.oracle.com/en/engineered-systems/exadata-database-machine/sagug/) — CellCLI, IORM configuration\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22175,"content_sha256":"b42ad661e54fbd3d60c63c4d16f626808fd5b5cc84a6c34dce51c2db0eff40a6"},{"filename":"architecture/inmemory-column-store.md","content":"# Oracle In-Memory Column Store\n\n## Overview\n\nThe Oracle Database In-Memory (DBIM) option, introduced in Oracle 12c Release 1 (12.1.0.2), adds a columnar memory store alongside the traditional row-format buffer cache. It is not a replacement for the row store — Oracle maintains both formats simultaneously and uses whichever is more efficient for a given operation. OLTP DML continues to operate against the row store; analytic queries that scan large portions of a table read from the columnar In-Memory store.\n\nThe primary benefit is analytic query acceleration: columnar storage, vectorized SIMD (Single Instruction Multiple Data) CPU instructions, and in-memory compression combine to accelerate full-scan analytic queries by 10x–100x compared to reading the same data from disk in row format.\n\nDBIM is an Oracle Database option — it requires a separate license unless you are using Oracle Exadata (where it is included) or Oracle Autonomous Database (where it is included and managed automatically).\n\nHowever, 19c+ permits 16GB of IN-MEMORY to be used per instance without requiring additional licensing options.\n---\n\n## 1. Architecture Overview\n\n### Dual-Format Storage\n\nOracle maintains two representations of data for In-Memory populated objects:\n\n| Format | Location | Used For |\n|---|---|---|\n| Row format | Buffer cache / disk | DML (INSERT, UPDATE, DELETE), PK/index lookups, small result sets |\n| Columnar format | In-Memory Column Store (IMCS) | Full-scan analytic queries, aggregations, range predicates |\n\nThe IMCS is a separate memory pool allocated from the SGA. It is configured with the `INMEMORY_SIZE` initialization parameter and exists independently of `DB_CACHE_SIZE`.\n\n### In-Memory Compression Units (IMCUs)\n\nThe IMCS stores data in **IMCUs (In-Memory Compression Units)**. Each IMCU:\n\n- Contains a contiguous range of rows from one column segment\n- Holds approximately 512,000 rows by default (adjustable)\n- Stores one column per IMCU partition (columnar layout)\n- Maintains a **min/max index** for each column within the IMCU (analogous to Exadata Storage Indexes)\n- Is compressed using one of the In-Memory compression formats\n\n### In-Memory Worker Processes\n\nPopulating objects into the IMCS is performed by background worker processes (`Wnnn`). These processes read segments from disk (or buffer cache) and populate the IMCS asynchronously. The number of worker processes is controlled by `INMEMORY_MAX_POPULATE_SERVERS`.\n\n---\n\n## 2. Configuring the In-Memory Column Store\n\n### Enabling DBIM\n\n```sql\n-- Set the IMCS size (minimum 100 MB; meaningful from 1 GB+)\n-- Requires database restart if not already set\nALTER SYSTEM SET INMEMORY_SIZE = 16G SCOPE = SPFILE;\n\n-- In 12.1, this requires a restart. In 12.2+, it can be increased dynamically\n-- (but cannot be decreased without a restart)\nALTER SYSTEM SET INMEMORY_SIZE = 24G; -- increase only, no restart needed in 12.2+\n\n-- Verify IMCS is active\nSELECT name, value\nFROM v$parameter\nWHERE name IN ('inmemory_size', 'inmemory_max_populate_servers',\n 'inmemory_query', 'inmemory_force')\nORDER BY name;\n\n-- Check IMCS memory area\nSELECT pool, alloc_bytes / 1024 / 1024 / 1024 AS alloc_gb,\n used_bytes / 1024 / 1024 / 1024 AS used_gb,\n populate_status\nFROM v$inmemory_area;\n```\n\n### In-Memory Initialization Parameters\n\n| Parameter | Default | Description |\n|---|---|---|\n| `INMEMORY_SIZE` | 0 (disabled) | Size of the IMCS; set to enable In-Memory |\n| `INMEMORY_MAX_POPULATE_SERVERS` | CPU count / 2 | Max background populate workers |\n| `INMEMORY_QUERY` | ENABLE | ENABLE / DISABLE In-Memory query at instance level |\n| `INMEMORY_FORCE` | DEFAULT | Force all tables into IMCS (PMEM, OFF, DEFAULT) |\n| `INMEMORY_VIRTUAL_COLUMNS` | MANUAL | AUTO: also populate virtual columns; MANUAL: user-specified |\n| `INMEMORY_CLAUSE_DEFAULT` | empty | Default INMEMORY attributes for all new objects |\n| `INMEMORY_DEEP_VECTORIZATION` | TRUE | Enable advanced SIMD vectorization |\n| `HEAT_MAP` | OFF | Required for Automatic Data Optimization (ADO) with INMEMORY |\n\n---\n\n## 3. Populating Objects into the IMCS\n\n### Table-Level INMEMORY Clause\n\n```sql\n-- Enable INMEMORY on a table with default settings (MEMCOMPRESS FOR QUERY LOW)\nALTER TABLE sales INMEMORY;\n\n-- Enable with a specific compression level\nALTER TABLE sales INMEMORY MEMCOMPRESS FOR QUERY HIGH;\n\n-- Enable for analytic workloads (highest compression, acceptable query overhead)\nALTER TABLE sales INMEMORY MEMCOMPRESS FOR CAPACITY HIGH;\n\n-- Priority controls when the object is populated relative to other objects\n-- CRITICAL > HIGH > MEDIUM > LOW > NONE (NONE = populate on first access only)\nALTER TABLE sales INMEMORY\n MEMCOMPRESS FOR QUERY LOW\n PRIORITY HIGH;\n\n-- Disable In-Memory for a specific table\nALTER TABLE sales NO INMEMORY;\n```\n\n### In-Memory Compression Levels\n\n| MEMCOMPRESS Level | Compression Method | Typical Ratio | CPU Cost |\n|---|---|---|---|\n| `FOR DML` | No compression | 1x | None |\n| `FOR QUERY LOW` | LZ4 (fast decompress) | 2x–4x | Very low |\n| `FOR QUERY HIGH` | ZLIB (fast decompress) | 4x–8x | Low |\n| `FOR CAPACITY LOW` | ZLIB (higher compression) | 8x–15x | Medium |\n| `FOR CAPACITY HIGH` | BZIP2-equivalent | 15x–25x | Higher |\n\n`FOR QUERY LOW` is the most common production choice: meaningful compression with near-zero decompression overhead during scans.\n\n### Selective Column Population\n\nNot all columns need to be in the IMCS. Excluding frequently-updated columns or rarely-queried columns reduces IMCS footprint.\n\n```sql\n-- Include only specific columns in the IMCS\nALTER TABLE sales\n INMEMORY MEMCOMPRESS FOR QUERY LOW\n (sale_date, region_id, product_id, amount, quantity)\n NO INMEMORY (customer_comments, audit_timestamp, last_updated_by);\n\n-- Verify column-level INMEMORY settings\nSELECT table_name, column_name, inmemory_compression\nFROM v$im_column_level\nWHERE table_name = 'SALES'\nORDER BY column_name;\n```\n\n### Partition-Level INMEMORY\n\nFor very large partitioned tables, populate only the \"hot\" partitions into the IMCS.\n\n```sql\n-- Apply INMEMORY to the current and previous month's partitions only\nALTER TABLE sales MODIFY PARTITION sales_202502 INMEMORY PRIORITY HIGH;\nALTER TABLE sales MODIFY PARTITION sales_202503 INMEMORY PRIORITY CRITICAL;\n\n-- Exclude older partitions from IMCS to conserve memory\nALTER TABLE sales MODIFY PARTITION sales_2024q1 NO INMEMORY;\nALTER TABLE sales MODIFY PARTITION sales_2024q2 NO INMEMORY;\nALTER TABLE sales MODIFY PARTITION sales_2024q3 NO INMEMORY;\nALTER TABLE sales MODIFY PARTITION sales_2024q4 NO INMEMORY;\n\n-- Check partition-level In-Memory status\nSELECT table_name, partition_name, inmemory,\n inmemory_priority, inmemory_compression\nFROM dba_tab_partitions\nWHERE table_name = 'SALES'\nORDER BY partition_position DESC\nFETCH FIRST 10 ROWS ONLY;\n```\n\n### Forcing Immediate Population\n\nBy default, objects with `PRIORITY NONE` are populated only on first access. Objects with other priorities are populated by background workers after DB startup. To populate immediately:\n\n```sql\n-- Force immediate population of a table (blocks until complete)\nEXEC DBMS_INMEMORY.POPULATE(schema_name => 'SCOTT', table_name => 'SALES');\n\n-- Force immediate population of a specific partition\nEXEC DBMS_INMEMORY.POPULATE(\n schema_name => 'SCOTT',\n table_name => 'SALES',\n subobject_name => 'SALES_202503'\n);\n\n-- Repopulate (useful after bulk DML that caused stale segments)\nEXEC DBMS_INMEMORY.REPOPULATE(schema_name => 'SCOTT', table_name => 'SALES', force => FALSE);\n```\n\n---\n\n## 4. Monitoring V$IM_SEGMENTS\n\n`V$IM_SEGMENTS` is the primary monitoring view for the In-Memory Column Store.\n\n```sql\n-- View all currently populated segments\nSELECT owner,\n segment_name,\n partition_name,\n segment_type,\n populate_status,\n bytes / 1024 / 1024 AS disk_mb,\n inmemory_size / 1024 / 1024 AS imcs_mb,\n bytes_not_populated / 1024 / 1024 AS not_populated_mb,\n ROUND(inmemory_size / NULLIF(bytes, 0) * 100, 1) AS imcs_to_disk_pct\nFROM v$im_segments\nORDER BY inmemory_size DESC;\n\n-- Check population status\n-- STARTED: populate worker has begun\n-- COMPLETED: fully populated\n-- FAILED: an error occurred during population\n-- OUT OF MEMORY: IMCS is full\nSELECT owner, segment_name, partition_name, populate_status\nFROM v$im_segments\nWHERE populate_status != 'COMPLETED'\nORDER BY owner, segment_name;\n\n-- IMCS usage summary\nSELECT COUNT(*) AS populated_segments,\n SUM(bytes) / 1024 / 1024 / 1024 AS total_disk_gb,\n SUM(inmemory_size) / 1024 / 1024 / 1024 AS total_imcs_gb,\n SUM(bytes_not_populated) / 1024 / 1024 AS not_populated_mb\nFROM v$im_segments;\n\n-- Objects marked INMEMORY but not yet in the store\nSELECT owner, segment_name, partition_name, priority\nFROM v$im_segments_detail\nWHERE populate_status = 'NOT POPULATED'\n AND priority != 'NONE'\nORDER BY priority DESC, owner, segment_name;\n```\n\n### IMCS Eviction and Repopulation\n\nThe IMCS uses an LRU-like eviction policy when it fills up. Lower-priority objects are evicted to make room for higher-priority ones.\n\n```sql\n-- Monitor IMCS pressure\nSELECT pool,\n alloc_bytes / 1024 / 1024 / 1024 AS alloc_gb,\n used_bytes / 1024 / 1024 / 1024 AS used_gb,\n ROUND(used_bytes / NULLIF(alloc_bytes, 0) * 100, 1) AS pct_used\nFROM v$inmemory_area;\n\n-- Check for evictions\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'IM populate segments requested',\n 'IM populate segments completed',\n 'IM repopulate (trickle) total',\n 'IM repopulate (trickle) failed',\n 'IM segments evicted',\n 'IM scans'\n)\nORDER BY name;\n```\n\n---\n\n## 5. Analytic Query Acceleration\n\nThe In-Memory Column Store dramatically accelerates queries with these patterns:\n\n### Full-Scan Aggregations\n\n```sql\n-- This query directly benefits from IMCS columnar scanning\n-- Oracle reads only the needed columns (region_id, sale_date, amount)\n-- in vectorized SIMD instructions across contiguous memory\nSELECT region_id,\n TRUNC(sale_date, 'MONTH') AS sale_month,\n COUNT(*) AS num_sales,\n SUM(amount) AS total_amount,\n AVG(amount) AS avg_amount,\n MAX(amount) AS max_amount\nFROM sales\nWHERE sale_date >= DATE '2025-01-01'\nGROUP BY region_id, TRUNC(sale_date, 'MONTH')\nORDER BY sale_month, region_id;\n\n-- Verify the query used IMCS via execution plan\nEXPLAIN PLAN FOR\nSELECT region_id, SUM(amount) FROM sales GROUP BY region_id;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Look for: TABLE ACCESS INMEMORY FULL\n-- Also look for: VECTOR GROUP BY (In-Memory Aggregation)\n```\n\n### Verifying In-Memory Access in Execution Plans\n\n```sql\n-- Session-level: confirm IMCS was used\nALTER SESSION SET STATISTICS_LEVEL = ALL;\n\nSELECT region_id, SUM(amount) FROM sales GROUP BY region_id;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));\n-- Key indicators in plan:\n-- Operation \"TABLE ACCESS INMEMORY FULL\" = IMCS scan\n-- Operation \"VECTOR GROUP BY\" = In-Memory aggregation\n-- \"In-Memory Scan\" note at bottom\n\n-- Force or disable IMCS at session level for testing\nALTER SESSION SET INMEMORY_QUERY = ENABLE; -- use IMCS (default)\nALTER SESSION SET INMEMORY_QUERY = DISABLE; -- bypass IMCS (test baseline)\n```\n\n### IMCS Join Acceleration\n\nThe In-Memory Column Store also accelerates hash joins by enabling Bloom filter creation and application inside the IMCS. This is the **In-Memory Join Group** feature (12.2+).\n\n```sql\n-- Join groups pre-compute hash values for join columns, enabling\n-- join predicate evaluation inside the IMCS without materializing rows\n\n-- Create a join group on a frequently-joined column pair\nCREATE INMEMORY JOIN GROUP jg_sales_products\n (sales(product_id), products(product_id));\n\n-- Verify join groups\nSELECT join_group_name, table_name, column_name\nFROM dba_joingroups\nORDER BY join_group_name, table_name;\n\n-- Queries joining on product_id now benefit from IMCS-level join processing\nSELECT p.product_name,\n SUM(s.amount) AS total_revenue\nFROM sales s\nJOIN products p ON s.product_id = p.product_id\nWHERE s.sale_date >= DATE '2025-01-01'\nGROUP BY p.product_name\nORDER BY total_revenue DESC;\n```\n\n---\n\n## 6. In-Memory Aggregation (Vector Group By)\n\nIn-Memory Aggregation (IMA) is a 12c+ feature that transforms GROUP BY operations to execute as SIMD vector operations across IMCU data. Instead of processing rows one at a time, Oracle accumulates aggregates across the entire IMCU using vectorized CPU instructions.\n\n### How Vector Group By Works\n\n1. IMCS scan produces a stream of column vectors from IMCUs\n2. For each IMCU, Oracle applies the GROUP BY in a vectorized pass, producing partial aggregates per IMCU\n3. Final aggregation merges the partial results from all IMCUs\n\nThis is orders of magnitude faster than traditional hash aggregation on row data because:\n- No row reconstruction (only needed columns are read)\n- SIMD: one CPU instruction processes 8+ values simultaneously\n- Cache-friendly: sequential memory access pattern\n\n```sql\n-- Verify Vector Group By is being used\nSELECT operation, options, object_name, cardinality, bytes, cost\nFROM plan_table\nWHERE operation IN ('VECTOR GROUP BY', 'HASH GROUP BY', 'TABLE ACCESS')\nORDER BY id;\n\n-- Enable/disable In-Memory Aggregation\nALTER SESSION SET \"_inmemory_vector_aggregation\" = TRUE; -- default\n\n-- Force Vector Group By hint\nSELECT /*+ VECTOR_TRANSFORM */\n sale_year, region_id, SUM(amount)\nFROM (SELECT EXTRACT(YEAR FROM sale_date) AS sale_year,\n region_id, amount\n FROM sales)\nGROUP BY sale_year, region_id;\n```\n\n---\n\n## 7. IMCS Sizing and Capacity Planning\n\n### Estimating IMCS Requirements\n\n```sql\n-- Estimate IMCS size for a table before enabling INMEMORY\n-- This executes a simulation to project the in-memory size\nSELECT\n table_name,\n SUM(bytes) / 1024 / 1024 AS on_disk_mb,\n -- Rule of thumb: IMCS size = disk_size * compression_ratio\n -- FOR QUERY LOW: ~0.3-0.5x disk size (compression + column orientation)\n ROUND(SUM(bytes) / 1024 / 1024 * 0.4, 0) AS estimated_imcs_mb_low,\n ROUND(SUM(bytes) / 1024 / 1024 * 0.2, 0) AS estimated_imcs_mb_high\nFROM dba_segments\nWHERE owner = 'SCOTT'\n AND segment_name IN ('SALES', 'ORDERS', 'PRODUCTS')\nGROUP BY table_name;\n\n-- After actual population, measure actual IMCS footprint\n-- (DBMS_INMEMORY_ADMIN does not have IMCS_BEGIN/IMCS_END procedures;\n-- use POPULATE_WAIT or enable INMEMORY on objects and monitor V$IM_SEGMENTS)\nSELECT SUM(inmemory_size) / 1024 / 1024 / 1024 AS total_imcs_gb\nFROM v$im_segments;\n\n-- Check how much IMCS has been consumed vs. allocated\nSELECT pool,\n alloc_bytes / 1024 / 1024 / 1024 AS alloc_gb,\n used_bytes / 1024 / 1024 / 1024 AS used_gb\nFROM v$inmemory_area;\n```\n\n### IMCS Sizing Guidelines\n\n| Scenario | Sizing Recommendation |\n|---|---|\n| All analytic tables in IMCS | Sum of (table_disk_size * compression_factor) + 20% overhead |\n| Hot partitions only | Size for current + previous period partitions only |\n| Mixed OLTP + analytics | 25–50% of active analytic data set; use priority to manage eviction |\n| Autonomous Database (ATP) | Oracle manages IMCS automatically; no user action needed |\n\n### Automatic In-Memory (AIM) in 19c+\n\nOracle 19c introduced Automatic In-Memory (AIM), which uses the Database Heat Map to automatically populate and evict objects from the IMCS based on actual access patterns.\n\n```sql\n-- Enable Heat Map (prerequisite for AIM)\nALTER SYSTEM SET HEAT_MAP = ON SCOPE = BOTH;\n\n-- Enable Automatic In-Memory management\nALTER SYSTEM SET INMEMORY_AUTOMATIC_LEVEL = MEDIUM SCOPE = BOTH;\n-- Levels: OFF (manual), LOW (only explicit INMEMORY objects managed),\n-- MEDIUM (AIM populates/evicts based on heat)\n\n-- Check AIM activity\nSELECT *\nFROM dba_inmemory_aimtasks\nFETCH FIRST 20 ROWS ONLY;\n\n-- Check Heat Map data\nSELECT object_name, owner, track_time,\n full_scan, lookup_scan, n_full_scans\nFROM v$heat_map_segment\nORDER BY full_scan DESC\nFETCH FIRST 20 ROWS ONLY;\n```\n\n---\n\n## 8. Best Practices\n\n- **Start with the most frequently scanned, most column-selective tables.** The IMCS yields the greatest benefit for wide tables (many columns) where queries access only a subset of columns. A narrow table (5 columns) with full-row access gains little.\n- **Use `PRIORITY CRITICAL` for tables critical to SLA.** During database startup, populate workers fill IMCS in priority order. CRITICAL objects are populated first, ensuring they are ready before user load arrives.\n- **Combine with partitioning for large tables.** Partition by time and keep only the \"hot\" partitions (current month, last 3 months) in the IMCS. This keeps IMCS footprint predictable and avoids eviction pressure.\n- **Do not enable INMEMORY on tables with heavy UPDATE activity.** The IMCS is eventually consistent with the row store via IMCU journaling. Very high DML rates cause the IMCU journal to fill up, triggering repopulation and consuming populate workers. Use IMCS for tables with low-to-moderate DML.\n- **Use join groups proactively.** If the same two tables are joined in >50% of analytic queries, a join group pre-computes hash values, enabling IMCS-level join processing without extracting and transferring rows to DB server buffers.\n- **Monitor `bytes_not_populated` in `V$IM_SEGMENTS`.** A non-zero value means the IMCS ran out of room and could not fully populate a segment. This means queries against unpopulated extents fall back to disk reads. Either increase `INMEMORY_SIZE` or reduce the set of in-memory objects.\n\n---\n\n## 9. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Not Increasing INMEMORY_SIZE After Adding Objects\n\nAdding `INMEMORY` to many tables without increasing `INMEMORY_SIZE` causes the IMCS to fill up. New objects fail to populate (or existing ones get evicted), and the query workload falls back to disk reads with no warning beyond a `populate_status = 'OUT OF MEMORY'` flag.\n\n```sql\n-- Check for OUT OF MEMORY populations\nSELECT owner, segment_name, partition_name, populate_status\nFROM v$im_segments\nWHERE populate_status = 'OUT OF MEMORY';\n\n-- Dynamically increase INMEMORY_SIZE if needed (12.2+, increase only)\nALTER SYSTEM SET INMEMORY_SIZE = 32G;\n```\n\n### Mistake 2: Enabling INMEMORY Without Enabling Parallel Query\n\nFor very large tables, the IMCS scan still benefits from parallel query (`PARALLEL` hint or `PARALLEL` clause on the table). Without parallel query, a serial IMCS scan on a 100 GB table is still a single-threaded operation. Pair IMCS with appropriate DOP.\n\n```sql\n-- Set table-level degree of parallelism for In-Memory scans\nALTER TABLE sales PARALLEL 8;\n\n-- Or use hint at query level\nSELECT /*+ PARALLEL(s, 8) */\n region_id, SUM(amount)\nFROM sales s\nGROUP BY region_id;\n```\n\n### Mistake 3: Assuming IMCS is Updated Synchronously\n\nThe IMCS is not updated in real time for every DML operation. Instead, Oracle uses IMCU journaling — DML changes are recorded in a small journal area within the IMCU, and periodic trickle repopulation merges the journal into the IMCU. Queries see the current (committed) data via journal merging, but there is a small window of extra CPU cost for highly concurrent DML tables.\n\nDo not use `SELECT ... AS OF TIMESTAMP` against IMCS objects if you expect exact change tracking — use the row store query path instead.\n\n### Mistake 4: Using CAPACITY Compression for Frequently Queried Tables\n\n`MEMCOMPRESS FOR CAPACITY HIGH` achieves the highest compression ratio but uses a slower decompression algorithm. For tables scanned hundreds of times per hour, the decompression CPU cost becomes significant. Use `FOR QUERY LOW` or `FOR QUERY HIGH` for hot analytical tables.\n\n### Mistake 5: Forgetting to REPOPULATE After Bulk Data Loads\n\nAfter a large `INSERT /*+ APPEND */` or partition exchange operation, the IMCS copy of that segment is stale. Oracle will trickle-repopulate it in the background, but until repopulation completes, queries against the new data read from disk. For time-sensitive workloads, trigger explicit repopulation immediately after load.\n\n```sql\n-- After nightly bulk load completes, repopulate explicitly\nEXEC DBMS_INMEMORY.REPOPULATE(\n schema_name => 'DW_OWNER',\n table_name => 'FACT_SALES',\n subobject_name => 'SALES_202503'\n);\n\n-- Monitor completion\nSELECT segment_name, partition_name, populate_status, bytes_not_populated\nFROM v$im_segments\nWHERE segment_name = 'FACT_SALES'\n AND partition_name = 'SALES_202503';\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database In-Memory Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/inmem/) — IMCS architecture, MEMCOMPRESS levels, population, monitoring\n- [DBMS_INMEMORY (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_INMEMORY.html) — POPULATE and REPOPULATE procedures (parameter is `subobject_name`, not `partition_name`)\n- [DBMS_INMEMORY_ADMIN (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_INMEMORY_ADMIN.html) — AIM parameters, FastStart, IME_CAPTURE_EXPRESSIONS\n- [Oracle Database Reference 19c — V$IM_SEGMENTS](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/V-IM_SEGMENTS.html) — In-Memory segment monitoring view\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21883,"content_sha256":"81601de51765e16fd1405c80a84471a81565f7edffe479d5dc715725809353a3"},{"filename":"architecture/multitenant.md","content":"# Oracle Multitenant Architecture: CDB and PDB Management\n\n## Overview\n\nOracle Multitenant, introduced in Oracle 12c, is an architectural paradigm that consolidates multiple databases into a single database engine while preserving their isolation. A **Container Database (CDB)** is the outer shell — one Oracle instance, one set of background processes, one shared SGA. Inside it live **Pluggable Databases (PDBs)**, each appearing to applications as a fully independent Oracle database.\n\nThe multitenant architecture solves three historical problems with Oracle consolidation:\n1. **Resource waste**: Running 50 separate single-tenant databases means 50 SGAs, 50 sets of background processes, and 50 sets of datafiles even when databases are small.\n2. **Patching complexity**: In non-CDB, each database requires its own patching lifecycle. In CDB, patching the CDB patches all PDBs simultaneously.\n3. **Provisioning speed**: Cloning a PDB from a template takes seconds rather than hours.\n\n---\n\n## 1. CDB Structure and Components\n\n### Root Container (CDB$ROOT)\n\nThe root container is the administrative hub of the CDB. It contains:\n- Oracle-supplied metadata (catalog, procedures, packages)\n- Common users (prefixed `C##` by default) and common roles\n- The common undo tablespace (in 12.2+ with shared undo mode)\n- System and sysaux tablespaces for the CDB itself\n\nThe root container is **not** a user data container. Never create application schemas in CDB$ROOT.\n\n### PDB$SEED\n\nPDB$SEED is a read-only template PDB. When you create a new PDB with `CREATE PLUGGABLE DATABASE ... FROM`, it is cloned from PDB$SEED by default (unless you specify a different source). PDB$SEED cannot be opened in read/write mode or modified.\n\n### User PDBs\n\nUser PDBs are the tenant containers where application data lives. Each PDB has:\n- Its own system tablespace (containing local data dictionary extensions)\n- Its own SYSAUX tablespace\n- Its own USERS (default) tablespace\n- Its own TEMP tablespace\n- Its own undo tablespace (in local undo mode, recommended)\n\n### CDB Architecture Query\n\n```sql\n-- View all containers and their status\nSELECT con_id, name, open_mode, restricted, con_uid\nFROM v$pdbs\nORDER BY con_id;\n\n-- See which container you are currently connected to\nSELECT sys_context('USERENV', 'CON_NAME') AS current_container,\n sys_context('USERENV', 'CON_ID') AS container_id\nFROM dual;\n\n-- Switch to a PDB (from CDB root, DBA privilege required)\nALTER SESSION SET CONTAINER = pdb_name;\n\n-- Switch back to CDB root\nALTER SESSION SET CONTAINER = CDB$ROOT;\n```\n\n---\n\n## 2. Creating and Managing PDBs\n\n### Creating a PDB from Seed\n\n```sql\n-- Simplest form: create from PDB$SEED with default settings\nCREATE PLUGGABLE DATABASE pdb_app1\n ADMIN USER pdb_admin IDENTIFIED BY \"SecurePass#1\"\n ROLES = (DBA)\n DEFAULT TABLESPACE users\n DATAFILE '/oradata/MYDB/pdb_app1/users01.dbf'\n SIZE 500M AUTOEXTEND ON NEXT 100M MAXSIZE 10G\n PATH_PREFIX = '/oradata/MYDB/pdb_app1/'\n FILE_NAME_CONVERT = ('/oradata/MYDB/pdbseed/', '/oradata/MYDB/pdb_app1/');\n\n-- Open the new PDB\nALTER PLUGGABLE DATABASE pdb_app1 OPEN;\n\n-- Make it auto-open on CDB startup\nALTER PLUGGABLE DATABASE pdb_app1 SAVE STATE;\n```\n\n### Creating a PDB with Storage Limits\n\n```sql\nCREATE PLUGGABLE DATABASE pdb_reporting\n ADMIN USER pdb_admin IDENTIFIED BY \"ReportPass#2\"\n STORAGE (MAXSIZE 50G MAXSHARED_TEMP_SIZE 5G)\n DEFAULT TABLESPACE app_data\n DATAFILE '/oradata/MYDB/pdb_reporting/app_data01.dbf'\n SIZE 1G AUTOEXTEND ON\n FILE_NAME_CONVERT = ('/oradata/MYDB/pdbseed/',\n '/oradata/MYDB/pdb_reporting/');\n\nALTER PLUGGABLE DATABASE pdb_reporting OPEN;\nALTER PLUGGABLE DATABASE pdb_reporting SAVE STATE;\n```\n\n### Opening and Closing PDBs\n\n```sql\n-- Open a single PDB in read/write mode\nALTER PLUGGABLE DATABASE pdb_app1 OPEN READ WRITE;\n\n-- Open all PDBs at once\nALTER PLUGGABLE DATABASE ALL OPEN;\n\n-- Open all PDBs except one\nALTER PLUGGABLE DATABASE ALL EXCEPT pdb_maintenance OPEN;\n\n-- Close a PDB (requires no active sessions by default)\nALTER PLUGGABLE DATABASE pdb_app1 CLOSE IMMEDIATE;\n\n-- Check current open mode of all PDBs\nSELECT name, open_mode, restricted\nFROM v$pdbs\nORDER BY name;\n```\n\n---\n\n## 3. Cloning PDBs\n\nCloning is one of the most powerful PDB operations. A PDB can be cloned locally within the same CDB, remotely from another CDB, or as a hot clone without closing the source.\n\n### Local Clone (same CDB)\n\n```sql\n-- Clone pdb_app1 to a new PDB pdb_app1_test\n-- Source PDB must be in READ ONLY mode for a cold clone\nALTER PLUGGABLE DATABASE pdb_app1 CLOSE IMMEDIATE;\nALTER PLUGGABLE DATABASE pdb_app1 OPEN READ ONLY;\n\nCREATE PLUGGABLE DATABASE pdb_app1_test\n FROM pdb_app1\n FILE_NAME_CONVERT = ('/oradata/MYDB/pdb_app1/',\n '/oradata/MYDB/pdb_app1_test/');\n\n-- Reopen the source in read/write\nALTER PLUGGABLE DATABASE pdb_app1 CLOSE IMMEDIATE;\nALTER PLUGGABLE DATABASE pdb_app1 OPEN READ WRITE;\n\n-- Open the clone\nALTER PLUGGABLE DATABASE pdb_app1_test OPEN;\n```\n\n### Hot Clone (no source downtime, 12.2+)\n\n```sql\n-- Hot clone requires the source to be in LOCAL UNDO mode\n-- and Archivelog enabled on the CDB\n\n-- Source stays open read/write during the clone\nCREATE PLUGGABLE DATABASE pdb_app1_hotclone\n FROM pdb_app1\n FILE_NAME_CONVERT = ('/oradata/MYDB/pdb_app1/',\n '/oradata/MYDB/pdb_app1_hotclone/')\n SNAPSHOT COPY; -- SNAPSHOT COPY uses sparse files (ASM/ACFS) for instant clone\n```\n\n### Remote Clone (from another CDB)\n\n```sql\n-- Create a database link pointing to the source CDB\nCREATE DATABASE LINK source_cdb_link\n CONNECT TO system IDENTIFIED BY \"SourcePass#\"\n USING 'SOURCE_CDB_TNSALIAS';\n\n-- Clone the remote PDB\nCREATE PLUGGABLE DATABASE pdb_migrated\n FROM pdb_source@source_cdb_link\n FILE_NAME_CONVERT = ('/oradata/SOURCECDB/pdb_source/',\n '/oradata/MYDB/pdb_migrated/');\n```\n\n---\n\n## 4. Plugging and Unplugging PDBs\n\nUnplug/plug is the mechanism for migrating a PDB between CDBs or for archiving a PDB.\n\n### Unplugging a PDB\n\n```sql\n-- Step 1: Close the PDB\nALTER PLUGGABLE DATABASE pdb_app1 CLOSE IMMEDIATE;\n\n-- Step 2: Unplug to an XML manifest file\nALTER PLUGGABLE DATABASE pdb_app1\n UNPLUG INTO '/tmp/pdb_app1_manifest.xml';\n\n-- Step 3: Drop the PDB from the current CDB (keeping datafiles)\nDROP PLUGGABLE DATABASE pdb_app1 KEEP DATAFILES;\n```\n\n### Plugging a PDB into a New CDB\n\n```sql\n-- Step 1: Verify compatibility before plugging in\n-- This check must be run AS SYSDBA in the target CDB\nDECLARE\n l_result VARCHAR2(4000);\nBEGIN\n l_result := DBMS_PDB.CHECK_PLUG_COMPATIBILITY(\n pdb_descr_file => '/tmp/pdb_app1_manifest.xml',\n pdb_name => 'pdb_app1'\n );\nEND;\n/\n\n-- Check compatibility results\nSELECT name, cause, type, message, status\nFROM PDB_PLUG_IN_VIOLATIONS\nWHERE name = 'PDB_APP1'\n AND status = 'PENDING';\n\n-- Step 2: Plug in the PDB\nCREATE PLUGGABLE DATABASE pdb_app1\n USING '/tmp/pdb_app1_manifest.xml'\n COPY -- COPY, NOCOPY, or MOVE\n FILE_NAME_CONVERT = ('/oradata/OLDCDB/pdb_app1/',\n '/oradata/NEWCDB/pdb_app1/')\n STORAGE UNLIMITED\n TEMPFILE REUSE;\n\n-- Step 3: Open the PDB (may trigger a run of catcon.pl for version mismatches)\nALTER PLUGGABLE DATABASE pdb_app1 OPEN\n UPGRADE; -- Use UPGRADE if source CDB version was lower\n```\n\n---\n\n## 5. Resource Management Between PDBs\n\nOracle Database Resource Manager (DBRM) in a CDB context manages CPU and I/O allocation across all PDBs to prevent one tenant from monopolizing resources.\n\n### CDB Resource Plan\n\n```sql\n-- Create the CDB-level resource plan\nBEGIN\n DBMS_RESOURCE_MANAGER.CREATE_PENDING_AREA();\n\n -- Create the CDB plan\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN(\n plan => 'CDB_PLAN_PROD',\n comment => 'Production CDB resource plan'\n );\n\n -- Assign shares and limits to individual PDBs\n -- shares: relative CPU weight (higher = more CPU when contention exists)\n -- utilization_limit: absolute CPU cap (% of total CDB CPU)\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN_DIRECTIVE(\n plan => 'CDB_PLAN_PROD',\n pluggable_database => 'PDB_APP1',\n shares => 4,\n utilization_limit => 80,\n parallel_server_limit => 60\n );\n\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN_DIRECTIVE(\n plan => 'CDB_PLAN_PROD',\n pluggable_database => 'PDB_REPORTING',\n shares => 2,\n utilization_limit => 40,\n parallel_server_limit => 30\n );\n\n -- Default directive for all other PDBs\n DBMS_RESOURCE_MANAGER.CREATE_CDB_PLAN_DIRECTIVE(\n plan => 'CDB_PLAN_PROD',\n pluggable_database => 'ORA$DEFAULT_PDB_DIRECTIVE',\n shares => 1,\n utilization_limit => 20,\n parallel_server_limit => 10\n );\n\n DBMS_RESOURCE_MANAGER.VALIDATE_PENDING_AREA();\n DBMS_RESOURCE_MANAGER.SUBMIT_PENDING_AREA();\nEND;\n/\n\n-- Activate the CDB plan\nALTER SYSTEM SET RESOURCE_MANAGER_PLAN = 'CDB_PLAN_PROD' SCOPE = BOTH;\n```\n\n### Per-PDB Resource Plans\n\nWithin each PDB, you can also define a standard DBRM consumer group plan that applies to sessions inside that PDB only.\n\n```sql\n-- Connect to the PDB\nALTER SESSION SET CONTAINER = pdb_app1;\n\n-- Create a PDB-level resource plan inside the PDB\nBEGIN\n DBMS_RESOURCE_MANAGER.CREATE_PENDING_AREA();\n\n DBMS_RESOURCE_MANAGER.CREATE_PLAN('PDB_APP1_PLAN', 'Internal PDB plan');\n\n DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP('OLTP_GROUP', 'OLTP users');\n DBMS_RESOURCE_MANAGER.CREATE_CONSUMER_GROUP('REPORT_GROUP', 'Report users');\n\n DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(\n plan => 'PDB_APP1_PLAN',\n group_or_subplan => 'OLTP_GROUP',\n mgmt_p1 => 80\n );\n\n DBMS_RESOURCE_MANAGER.CREATE_PLAN_DIRECTIVE(\n plan => 'PDB_APP1_PLAN',\n group_or_subplan => 'REPORT_GROUP',\n mgmt_p2 => 20\n );\n\n DBMS_RESOURCE_MANAGER.SUBMIT_PENDING_AREA();\nEND;\n/\n\nALTER SYSTEM SET RESOURCE_MANAGER_PLAN = 'PDB_APP1_PLAN' SCOPE = BOTH;\n```\n\n### Memory Isolation per PDB (19c+)\n\n```sql\n-- Set SGA and PGA memory limits per PDB (requires 19c Multitenant license)\nALTER SESSION SET CONTAINER = pdb_app1;\nALTER SYSTEM SET SGA_TARGET = 8G SCOPE = BOTH;\nALTER SYSTEM SET SGA_MIN_SIZE = 4G SCOPE = BOTH;\n\nALTER SESSION SET CONTAINER = pdb_reporting;\nALTER SYSTEM SET SGA_TARGET = 4G SCOPE = BOTH;\n\n-- Verify inside each PDB after switching containers\nSHOW PARAMETER sga_target;\nSHOW PARAMETER sga_min_size;\n```\n\n---\n\n## 6. Common Users and Local Users\n\nUser types in a CDB are one of the most confusing aspects of multitenant. Misunderstanding this distinction leads to security and connectivity problems.\n\n### Common Users\n\nCommon users are created in CDB$ROOT and exist (with the same identity) in all containers. They are distinguished by the `C##` prefix enforced by Oracle.\n\n```sql\n-- Connect to CDB$ROOT as SYSDBA\nALTER SESSION SET CONTAINER = CDB$ROOT;\n\n-- Create a common DBA\nCREATE USER c##dba_admin IDENTIFIED BY \"AdminPass#1\"\n CONTAINER = ALL;\n\n-- Grant a common privilege across all containers\nGRANT DBA TO c##dba_admin CONTAINER = ALL;\n\n-- Grant privilege only in one specific container\nGRANT READ ANY TABLE TO c##dba_admin CONTAINER = CURRENT;\n```\n\n### Local Users\n\nLocal users exist in exactly one PDB and cannot see or be seen from other PDBs or CDB$ROOT.\n\n```sql\n-- Connect to the target PDB first\nALTER SESSION SET CONTAINER = pdb_app1;\n\n-- Create a local user (no C## prefix required)\nCREATE USER app_owner IDENTIFIED BY \"AppPass#1\";\nGRANT CREATE SESSION, CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE SEQUENCE TO app_owner;\nGRANT UNLIMITED TABLESPACE TO app_owner;\n```\n\n### Common Roles and Local Roles\n\nThe same distinction applies to roles. Oracle-supplied roles (DBA, SYSDBA, etc.) are common roles. Application-specific roles should be created as local roles within their PDB.\n\n```sql\n-- Common role (created in CDB$ROOT, available everywhere)\nALTER SESSION SET CONTAINER = CDB$ROOT;\nCREATE ROLE c##common_readonly CONTAINER = ALL;\nGRANT SELECT ANY TABLE TO c##common_readonly CONTAINER = ALL;\n\n-- Local role (inside the PDB)\nALTER SESSION SET CONTAINER = pdb_app1;\nCREATE ROLE app_readonly;\nGRANT SELECT ON app_owner.orders TO app_readonly;\nGRANT SELECT ON app_owner.customers TO app_readonly;\n```\n\n---\n\n## 7. Application Containers (19c)\n\nApplication containers are a second level of containment inside a CDB. An Application Container acts as a root for a set of Application PDBs, allowing application-specific master data (seed data, reference tables, even PL/SQL objects) to be shared across all application PDBs.\n\n```\nCDB$ROOT\n├── Application Container: APP_MASTER\n│ ├── APP_SEED (read-only template)\n│ ├── APP_PDB_TENANT1 (tenant data + inherited app objects)\n│ ├── APP_PDB_TENANT2\n│ └── APP_PDB_TENANT3\n└── PDB_OTHER (regular PDB, not in app container)\n```\n\n### Creating an Application Container\n\n```sql\n-- Step 1: Create the Application Container in CDB$ROOT\nALTER SESSION SET CONTAINER = CDB$ROOT;\n\nCREATE PLUGGABLE DATABASE saas_app_root\n AS APPLICATION CONTAINER\n ADMIN USER saas_admin IDENTIFIED BY \"SaasAdmin#1\"\n FILE_NAME_CONVERT = ('/oradata/MYDB/pdbseed/',\n '/oradata/MYDB/saas_app_root/');\n\nALTER PLUGGABLE DATABASE saas_app_root OPEN;\n\n-- Step 2: Connect to the Application Container and install the application\nALTER SESSION SET CONTAINER = saas_app_root;\n\nALTER PLUGGABLE DATABASE APPLICATION saas_crm BEGIN INSTALL '1.0';\n\n-- Create shared application objects (visible to all app PDBs)\nCREATE TABLE app_config (\n config_key VARCHAR2(100) NOT NULL,\n config_value VARCHAR2(4000),\n CONSTRAINT pk_app_config PRIMARY KEY (config_key)\n) SHARING = DATA; -- DATA sharing: metadata AND rows shared\n\nCREATE TABLE product_catalog (\n product_id NUMBER NOT NULL,\n product_name VARCHAR2(200) NOT NULL,\n CONSTRAINT pk_product PRIMARY KEY (product_id)\n) SHARING = EXTENDED DATA; -- EXTENDED DATA: shared rows + tenants can add their own\n\nALTER PLUGGABLE DATABASE APPLICATION saas_crm END INSTALL '1.0';\n```\n\n### Table Sharing Options in Application Containers\n\n| SHARING Clause | Metadata Shared | Data Shared | Tenant Can Add Rows |\n|---|---|---|---|\n| `METADATA` | Yes | No | Yes (private rows) |\n| `DATA` | Yes | Yes | No |\n| `EXTENDED DATA` | Yes | Yes | Yes (private rows visible only to tenant) |\n| `NONE` | No | No | N/A (tenant-local table) |\n\n---\n\n## 8. Best Practices\n\n- **Always use Local Undo mode (12.2+).** Shared Undo (the 12.1 default) means all undo for all PDBs is in one undo tablespace, making PDB-level point-in-time recovery much more difficult. Local undo places each PDB's undo in its own undo tablespace.\n- **Use `SAVE STATE` for PDBs that should auto-open.** Without `SAVE STATE`, PDBs close when the CDB instance restarts and stay closed until manually opened.\n- **Set storage limits on PDBs early.** Without `STORAGE (MAXSIZE ...)`, one runaway PDB can fill all shared disk, impacting all other PDBs.\n- **Use a CDB-level resource plan from day one.** Without a resource plan, CPU allocation is first-come-first-served, and a heavy batch PDB can starve OLTP PDBs.\n- **Apply patches at the CDB level.** One `opatch apply` patches all PDBs at once. This is a major operational advantage over non-CDB environments.\n- **Keep CDB$ROOT clean.** No application data, no application schemas. CDB$ROOT should contain only common administrative objects.\n- **Use separate CDBs for different lifecycle stages.** Put DEV PDBs in a DEV CDB and PROD PDBs in a PROD CDB. The CDB-level patch level and parameter settings apply to all contained PDBs.\n\n---\n\n## 9. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Creating Application Schemas in CDB$ROOT\n\nBecause CDB$ROOT is open by default and easy to connect to as SYSDBA, developers sometimes create their application tables in the root container. These objects are shared metadata objects — they behave unpredictably and are nearly impossible to clean up.\n\n**Fix:** Enforce a policy that application schemas are only created inside designated PDBs. Use `PDB_LOCKDOWN` profiles to prevent non-administrative connections to CDB$ROOT.\n\n### Mistake 2: Not Using `CONTAINER = ALL` When Granting Common Privileges\n\nA common user granted a privilege with `CONTAINER = CURRENT` (while logged into CDB$ROOT) has that privilege only in CDB$ROOT, not in any PDB. This is a common source of \"privilege not granted\" errors when the common user connects directly to a PDB.\n\n```sql\n-- WRONG: grant only applies to CDB$ROOT\nGRANT DBA TO c##dba_admin;\n\n-- CORRECT: grant applies to all current and future containers\nGRANT DBA TO c##dba_admin CONTAINER = ALL;\n```\n\n### Mistake 3: Forgetting to Set `LOCAL_LISTENER` Inside Each PDB\n\nWhen PDBs need to register with different listeners (e.g., on different ports for different applications), each PDB must set its own `LOCAL_LISTENER` parameter.\n\n```sql\nALTER SESSION SET CONTAINER = pdb_app1;\nALTER SYSTEM SET LOCAL_LISTENER = '(ADDRESS=(PROTOCOL=TCP)(HOST=myhost)(PORT=1521))' SCOPE = BOTH;\n```\n\n### Mistake 4: Using IMPDP/EXPDP Without Specifying the PDB\n\nData Pump defaults to the CDB root when run from the OS. Always connect through the PDB service or specify the appropriate connection string.\n\n```bash\n# WRONG: imports into CDB$ROOT\n# impdp system/pwd directory=DATA_PUMP_DIR dumpfile=mydata.dmp\n\n# CORRECT: imports into the specific PDB\n# impdp system/pwd@//host:1521/pdb_app1 directory=DATA_PUMP_DIR dumpfile=mydata.dmp\n```\n\n### Mistake 5: Cloning a PDB Without Verifying the Compatibility Report\n\nPlugging a PDB from a lower version CDB into a higher version CDB without running `DBMS_PDB.CHECK_PLUG_COMPATIBILITY` and resolving violations results in an incompatible PDB that either fails to open or opens with errors. Always review `PDB_PLUG_IN_VIOLATIONS` before opening a newly plugged PDB.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Multitenant Administrator's Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/) — CDB/PDB architecture, creating and managing PDBs, cloning, plugging/unplugging, common users, Application Containers\n- [DBMS_PDB (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_PDB.html) — CHECK_PLUG_COMPATIBILITY procedure\n- [DBMS_RESOURCE_MANAGER (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_RESOURCE_MANAGER.html) — CREATE_CDB_PLAN, CREATE_CDB_PLAN_DIRECTIVE procedures\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19222,"content_sha256":"61ba841ef2169dce9046f72f89991f54b54f959060f233e49994624fe0ce4581"},{"filename":"architecture/oracle-cloud-oci.md","content":"# Oracle Database on OCI: Cloud Services Reference\n\n## Overview\n\nOracle Cloud Infrastructure (OCI) offers a spectrum of database services ranging from fully self-managed virtual machine databases to fully autonomous, self-driving databases. Understanding which service tier fits a workload requires knowing the trade-offs in control, automation, cost, and performance for each option.\n\nThe three primary Oracle Database service families on OCI are:\n\n| Service Family | Management Level | Best For |\n|---|---|---|\n| Autonomous Database (ATP/ADW) | Fully managed + self-tuning | New apps, analytics, dev/test, cost-sensitive |\n| Base Database Service (DBCS) | Infrastructure managed; DB managed by you | Lift-and-shift, existing apps, full DBA control |\n| Exadata Cloud Service (ExaCS) | Infrastructure managed; DB managed by you | Mission-critical OLTP, large-scale consolidation |\n\n---\n\n## 1. Autonomous Transaction Processing (ATP)\n\nATP is Oracle's fully managed OLTP database service. Oracle manages the infrastructure, patching, backup, tuning, and availability. Customers manage schemas, SQL, and application connectivity.\n\n### Key Characteristics\n\n- **Workload type:** Mixed OLTP + operational reporting\n- **Storage format:** Row store by default, with In-Memory Column Store enabled automatically for eligible objects\n- **Compute model:** OCPU-based (1 OCPU = 2 vCPUs); billed per OCPU-hour\n- **Connection security:** mTLS enforced by default; wallet-based connections\n- **Oracle version:** Latest Oracle 19c (or higher) with automated quarterly patching\n- **Concurrency model:** 3 pre-configured services — `_tp` (low latency), `_tpurgent` (highest priority), `_low` (background)\n\n### Connecting to ATP\n\n```sql\n-- ATP requires Oracle Wallet for authentication (mTLS)\n-- Download the wallet (Instance Wallet or Regional Wallet) from OCI Console\n-- or using OCI CLI:\n-- oci db autonomous-database generate-wallet --autonomous-database-id \u003cocid> --file wallet.zip --password WalletPass#1\n\n-- sqlplus connection using TNS_ADMIN pointing to wallet directory\n-- export TNS_ADMIN=/home/oracle/wallet\n-- sqlplus admin/YourPass#1@myatp_tp\n\n-- JDBC connection string for applications\n-- jdbc:oracle:thin:@myatp_tp?TNS_ADMIN=/wallet_dir\n\n-- Check service names available in ATP\nSELECT name, network_name, goal\nFROM v$active_services\nWHERE name NOT IN ('SYS$BACKGROUND', 'SYS$USERS')\nORDER BY name;\n```\n\n### ATP-Specific Features\n\n```sql\n-- Autonomous Database automatically creates performance indexes\n-- View auto-created indexes\nSELECT index_name, table_name, visibility, status, auto\nFROM user_indexes\nWHERE auto = 'YES'\nORDER BY table_name;\n\n-- Check which visible tables are partitioned\nSELECT table_name, partitioned\nFROM user_tables\nORDER BY table_name;\n\nSELECT table_name, partitioning_type, subpartitioning_type\nFROM user_part_tables\nORDER BY table_name;\n\n-- Machine Learning features in ATP\n-- Oracle ML is pre-installed\nSELECT *\nFROM user_mining_models;\n```\n\n---\n\n## 2. Autonomous Data Warehouse (ADW)\n\nADW is Oracle's fully managed analytical database service. It is architected for parallel query execution, columnar storage, and BI/reporting workloads.\n\n### Key Differences from ATP\n\n| Feature | ATP | ADW |\n|---|---|---|\n| Primary workload | OLTP, mixed | Analytics, DW, reporting |\n| Default parallelism | Low (OLTP-appropriate) | High (auto parallel query) |\n| Default In-Memory | Selective | Aggressive (for eligible objects) |\n| Auto-indexing | Enabled | Disabled (analytics prefers FTS) |\n| Default compression | Advanced Row Compression | HCC Query High |\n| Default service | `_tp` | `_high`, `_medium`, `_low` |\n\n### ADW Connection Services\n\nADW provides three pre-defined services with different resource profiles:\n\n| Service | Parallelism | Priority | Use Case |\n|---|---|---|---|\n| `_high` | Max DOP | Highest | Single critical query |\n| `_medium` | Moderate DOP | Medium | Standard BI/reporting |\n| `_low` | Minimal | Lowest | ETL, data loads, background |\n\n```sql\n-- Connect using the appropriate service for workload type\n-- For BI tool connections: @myinstance_medium\n-- For ETL loads: @myinstance_low\n-- For ad-hoc critical queries: @myinstance_high\n\n-- Check current resource group assignments\nSELECT username, resource_consumer_group\nFROM v$session\nWHERE type = 'USER'\nORDER BY username;\n\n-- ADW automatically applies HCC compression to new tables\n-- Check compression on ADW tables\nSELECT table_name, compression, compress_for\nFROM user_tables\nORDER BY table_name;\n```\n\n---\n\n## 3. Auto-Scaling and Auto-Backup\n\n### Auto-Scaling (Compute)\n\nAutonomous Databases support automatic scaling of compute resources without downtime when the database is under CPU pressure.\n\n```sql\n-- Scaling is managed via OCI Console, CLI, or REST API\n-- OCI CLI to enable auto-scaling:\n-- oci db autonomous-database update \\\n-- --autonomous-database-id \u003cocid> \\\n-- --is-auto-scaling-enabled true\n\n-- Monitor CPU usage to understand scaling behavior\nSELECT end_interval_time,\n ROUND(AVG(value), 2) AS avg_cpu_pct\nFROM dba_hist_sysmetric_summary\nWHERE metric_name = 'CPU Usage Per Sec'\n AND end_interval_time >= SYSTIMESTAMP - INTERVAL '24' HOUR\nGROUP BY end_interval_time\nORDER BY end_interval_time DESC\nFETCH FIRST 48 ROWS ONLY;\n\n-- Autonomous Database scales between baseCPU and 3x baseCPU automatically\n-- Current OCPU allocation visible in:\nSELECT name, value\nFROM v$parameter\nWHERE name IN ('cpu_count', 'parallel_threads_per_cpu')\nORDER BY name;\n```\n\n### Auto-Backup\n\nAutonomous Databases perform automatic daily backups to OCI Object Storage with a 60-day retention period by default.\n\n```sql\n-- OCI CLI: list available backups for an Autonomous Database\n-- oci db autonomous-database-backup list \\\n-- --autonomous-database-id \u003cocid>\n\n-- OCI CLI: restore to a specific point in time\n-- oci db autonomous-database restore \\\n-- --autonomous-database-id \u003cocid> \\\n-- --timestamp \"2026-03-01T12:00:00.000Z\"\n\n-- View backup history from within the database\nSELECT input_type, status, start_time, end_time,\n input_bytes / 1024 / 1024 / 1024 AS input_gb,\n output_bytes / 1024 / 1024 / 1024 AS output_gb\nFROM v$rman_backup_job_details\nORDER BY start_time DESC\nFETCH FIRST 10 ROWS ONLY;\n```\n\n---\n\n## 4. Base Database Service (DBCS)\n\nBase Database Service provisions Oracle Database on OCI compute shapes (Virtual Machine or Bare Metal). Oracle manages the underlying infrastructure (OS patching, storage provisioning), while the DBA retains full control over the database.\n\n### VM DB System vs. Bare Metal DB System\n\n| Aspect | VM DB System | BM DB System |\n|---|---|---|\n| Compute | Shared or dedicated VM | Full bare metal node |\n| Storage | Iscsi block volumes | NVMe local SSD + block |\n| RAC support | Up to 2 nodes (RAC Two-Node) | Up to 2 nodes |\n| Starting size | 1 OCPU | 24+ OCPUs |\n| Use case | Dev, test, smaller prod | Large OLTP, high I/O |\n\n### Provisioning Considerations\n\n```sql\n-- After provisioning via OCI Console or CLI, verify DB configuration\nSELECT name, db_unique_name, log_mode, open_mode,\n flashback_on, force_logging, platform_name\nFROM v$database;\n\n-- Check storage usage on DBCS (block volumes appear as ASM disk groups)\nSELECT group_number, name, type, state, total_mb, free_mb,\n ROUND((total_mb - free_mb) / total_mb * 100, 1) AS pct_used\nFROM v$asm_diskgroup\nORDER BY name;\n\n-- DBCS includes Data Guard by default for Enterprise Edition High Performance\n-- Check Data Guard configuration\nSELECT db_unique_name, role, open_mode, protection_mode, protection_level\nFROM v$database;\n```\n\n### DBCS-Specific Operations\n\n```sql\n-- Enable or verify archivelog mode (required for backups and Data Guard)\nARCHIVE LOG LIST;\n\n-- RMAN backup on DBCS (Oracle manages backup to OCI Object Storage or local)\n-- The OCI backup plugin (bkup_api) is pre-installed on DBCS\n-- Manual RMAN backup to OCI Object Storage:\n-- RMAN> CONFIGURE CHANNEL DEVICE TYPE SBT\n-- PARMS='SBT_LIBRARY=/opt/oracle/dcs/commonstore/pkgrepos/oss/odbcs/libopc.so\n-- ENV=(OPC_PFILE=/opt/oracle/dcs/commonstore/objectstore/config/opctest.ora)';\n-- RMAN> BACKUP DATABASE PLUS ARCHIVELOG;\n\n-- DBCS patching uses DBA console (OCI console) or dbaascli utility\n-- dbaascli dbpatch apply --db MYDB --patch_id \u003cpatch_id>\n```\n\n---\n\n## 5. Exadata Cloud Service (ExaCS)\n\nExaCS brings the full Exadata hardware platform (including Smart Scan, Storage Indexes, and HCC) to OCI. Oracle manages the Exadata hardware and grid infrastructure; the customer manages the database.\n\n### ExaCS Infrastructure Options\n\n| Option | Description |\n|---|---|\n| Quarter Rack | 2 DB servers, 3 storage cells |\n| Half Rack | 4 DB servers, 6 storage cells |\n| Full Rack | 8 DB servers, 12 storage cells |\n| Elastic Configurations (X9M+) | Choose DB server and storage cell count independently |\n\n### ExaCS vs. ExaDB-C@C (Exadata Cloud at Customer)\n\n- **ExaCS**: Exadata hardware resides in OCI data centers. Customer manages the DB; Oracle manages Exadata infrastructure.\n- **ExaDB-C@C**: Exadata hardware installed at the customer's on-premises data center; Oracle manages infrastructure remotely; customer manages the DB.\n\n```sql\n-- Verify Exadata features are active\nSELECT name, value\nFROM v$parameter\nWHERE name IN ('cell_offload_processing',\n 'cell_offload_compaction',\n 'cell_offload_plan_display',\n 'enable_goldengate_replication')\nORDER BY name;\n\n-- Confirm Smart Scan is being used\nSELECT name, value\nFROM v$sysstat\nWHERE name IN (\n 'cell physical IO interconnect bytes',\n 'cell physical IO interconnect bytes returned by smart scan',\n 'cell scans'\n)\nORDER BY name;\n```\n\n---\n\n## 6. OCI Connection Methods\n\n### Standard Connections (Non-Autonomous)\n\n```sql\n-- Standard JDBC connection to DBCS\n-- jdbc:oracle:thin:@//host:1521/service_name\n\n-- TNS-based connection\n-- HOST_PORT_SN =\n-- (DESCRIPTION =\n-- (ADDRESS = (PROTOCOL = TCP)(HOST = mydbcs-host.subnet.vcn.oraclevcn.com)(PORT = 1521))\n-- (CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME = mydb.subnet.vcn.oraclevcn.com))\n-- )\n\n-- Check listening services on DBCS\nSELECT name, network_name\nFROM v$active_services\nWHERE name NOT IN ('SYS$BACKGROUND', 'SYS$USERS')\nORDER BY name;\n```\n\n### Wallet-Based Connections (Autonomous Database)\n\n```sql\n-- Three wallet types for Autonomous Databases:\n-- 1. Instance Wallet: specific to one DB instance\n-- 2. Regional Wallet: works with any Autonomous DB in the region\n-- 3. mTLS (mutual TLS): two-way certificate authentication\n\n-- TLS-only connection (wallet not required, 21c and later ADB)\n-- Disable wallet requirement for TLS-only connections:\n-- oci db autonomous-database update \\\n-- --autonomous-database-id \u003cocid> \\\n-- --is-mtls-connection-required false\n\n-- JDBC Easy Connect Plus syntax (no wallet, TLS only)\n-- jdbc:oracle:thin:@myatp.adb.us-ashburn-1.oraclecloud.com:1522/dbname_tp.adb.oraclecloud.com\n\n-- Verify the current session's network service banner\nSELECT network_service_banner\nFROM v$session_connect_info\nWHERE sid = SYS_CONTEXT('USERENV', 'SID');\n```\n\n### Private Endpoint Connections\n\nOCI supports connecting to Autonomous Databases from within a VCN using Private Endpoints, eliminating internet exposure:\n\n```sql\n-- Private endpoint forces all connections through the VCN\n-- Configured at provisioning time or added post-provisioning via OCI Console\n\n-- After enabling private endpoint, check endpoint details:\n-- oci db autonomous-database get --autonomous-database-id \u003cocid> \\\n-- --query 'data.{\"private-endpoint\": \"private-endpoint\", \"private-ip\": \"private-ip\"}'\n\n-- Connection string for private endpoint uses private IP or private DNS name\n-- jdbc:oracle:thin:@10.0.1.25:1521/myatp_tp\n```\n\n---\n\n## 7. Oracle Cloud Free Tier\n\nOracle Cloud Free Tier provides two Autonomous Databases (1 OCPU, 20 GB storage each) permanently free with no time limit, plus $300 in credits for other services for 30 days.\n\n### Free Tier Limitations\n\n| Resource | Always Free Limit |\n|---|---|\n| Autonomous DB instances | 2 (one ATP, one ADW) |\n| OCPUs per instance | 1 (no auto-scaling) |\n| Storage per instance | 20 GB |\n| APEX workspaces | Included |\n| Oracle ML notebooks | Included |\n| Data loading | REST, APEX, DB Actions |\n| Backup | 60 days included |\n\n```sql\n-- Connecting to Free Tier Autonomous Database\n-- Service names follow the same pattern as paid ADB\n-- Available services: _tp, _tpurgent, _low, _high, _medium\n\n-- Verify Free Tier resource constraints\nSELECT name, value\nFROM v$parameter\nWHERE name IN ('cpu_count', 'sga_max_size', 'pga_aggregate_target')\nORDER BY name;\n\n-- Free Tier comes with Oracle APEX pre-installed\nSELECT version_no\nFROM apex_release;\n\n-- ORDS is pre-enabled in Free Tier; verify access via the APEX / Database Actions URLs in OCI Console\n```\n\n---\n\n## 8. Cloud-Specific Features Summary\n\n### Autonomous Database Operational Commands (OCI CLI)\n\n```bash\n# Start / Stop Autonomous Database\noci db autonomous-database start --autonomous-database-id \u003cocid>\noci db autonomous-database stop --autonomous-database-id \u003cocid>\n\n# Scale OCPUs (no downtime)\noci db autonomous-database update \\\n --autonomous-database-id \u003cocid> \\\n --cpu-core-count 8\n\n# Scale storage (no downtime, can only increase)\noci db autonomous-database update \\\n --autonomous-database-id \u003cocid> \\\n --data-storage-size-in-tbs 2\n\n# Clone an Autonomous Database\noci db autonomous-database create-from-clone \\\n --clone-type FULL \\\n --source-id \u003csource_ocid> \\\n --display-name MyATP_Clone \\\n --db-name MYATPCLONE \\\n --admin-password \"ClonePass#1\" \\\n --compartment-id \u003ccompartment_ocid>\n```\n\n### Database Actions (SQL Developer Web)\n\nAll Autonomous Database instances include Database Actions (formerly SQL Developer Web), a browser-based SQL and administration IDE accessible at:\n\n```\nhttps://\u003cadb_host>/ords/sql-developer\n```\n\nKey Database Actions modules:\n- SQL Worksheet — interactive SQL execution\n- Data Load — upload CSV/JSON/Parquet directly to tables\n- Data Studio — business intelligence, data insights\n- Oracle ML — Jupyter-compatible ML notebooks\n- Oracle APEX — full application development platform\n\n---\n\n## 9. Best Practices\n\n- **Use Private Endpoints for all production Autonomous Databases.** Public endpoints expose the database to the internet. Private endpoints restrict access to your VCN and eliminate the need for IP allowlisting.\n- **Use the `_medium` service for BI tool connections to ADW.** The `_high` service uses maximum parallelism, which works well for single queries but can cause contention when many BI users are active simultaneously.\n- **Enable auto-scaling during initial deployment and disable it only after load testing.** It is far better to discover that auto-scaling is needed during testing than after a production load spike causes connection timeouts.\n- **Store wallet files securely.** Autonomous Database wallets contain private keys. Never commit wallet files to source control. Use OCI Vault for secret management in CI/CD pipelines.\n- **Right-size DBCS before migrating to Autonomous.** Test the application on ATP using the same data volume and workload profile. Autonomous's automatic optimizations can change query plans; validate execution plans after migration.\n- **Use Data Safe for Autonomous Database security posture management.** Data Safe (included with ADB) provides security assessments, user assessments, data masking, and activity auditing with no additional configuration.\n\n---\n\n## 10. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Connecting to ATP with `_high` Service for OLTP Applications\n\nThe `_high` service in ATP uses maximum DOP, which causes parallel query overhead and resource contention for short OLTP transactions. Use `_tp` (low-latency, no parallelism) for OLTP and `_tpurgent` for priority transactions.\n\n### Mistake 2: Not Rotating Wallets After Security Events\n\nWallets do not expire automatically. After a security incident or staff change, download and deploy a new wallet. The old wallet remains valid until explicitly invalidated by rotating the database password.\n\n```bash\n# Rotate the ADMIN password (invalidates the old wallet on next download)\noci db autonomous-database update \\\n --autonomous-database-id \u003cocid> \\\n --admin-password \"NewSecurePass#2\"\n# Then re-download the wallet\noci db autonomous-database generate-wallet \\\n --autonomous-database-id \u003cocid> \\\n --file new_wallet.zip \\\n --password \"WalletPass#2\"\n```\n\n### Mistake 3: Assuming DBCS Patches Are Automatic\n\nUnlike Autonomous Databases, DBCS patches are **not** applied automatically. The DBA must apply quarterly DB patches manually through the OCI Console (one-click patching) or via `dbaascli`. Unpatched DBCS instances accumulate CVEs over time.\n\n### Mistake 4: Using Autonomous Database for Workloads Requiring Custom Initialization Parameters\n\nATP and ADW do not allow modification of most `init.ora` parameters. Workloads that require specific `optimizer_features_enable`, custom `event` settings, or non-standard memory parameters are not suitable for Autonomous Database. Use DBCS or ExaCS for those workloads.\n\n### Mistake 5: Ignoring Egress Costs When Loading Data\n\nDownloading large datasets from Autonomous Database to on-premises incurs OCI egress charges. Use OCI Object Storage as an intermediate stage (egress between ADB and OCI Object Storage in the same region is free) and then transfer from Object Storage to on-premises.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Autonomous Database Documentation](https://docs.oracle.com/en/cloud/paas/autonomous-database/) — ATP, ADW, auto-scaling, auto-backup, wallet connections\n- [Oracle Base Database Service Documentation](https://docs.oracle.com/en/cloud/paas/base-database/) — DBCS provisioning, VM vs. BM, patching\n- [Oracle Exadata Cloud Service Documentation](https://docs.oracle.com/en/engineered-systems/exadata-cloud-service/) — ExaCS infrastructure options\n- [OCI CLI Reference](https://docs.oracle.com/en-us/iaas/tools/oci-cli/latest/) — autonomous-database commands\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18609,"content_sha256":"4abe62c40c349bd66fc7153dff12e50292c414171083b4ebe43280d81ae33a3c"},{"filename":"architecture/rac-concepts.md","content":"# Oracle Real Application Clusters (RAC) Architecture\n\n## Overview\n\nOracle Real Application Clusters (RAC) is a shared-disk cluster database technology that allows multiple Oracle instances to mount and open the same database simultaneously. Each node runs its own Oracle instance (memory structures + background processes), but all nodes share the same set of datafiles stored on shared storage (ASM or a cluster file system). RAC provides high availability through instance survivability and horizontal scalability by distributing workload across nodes.\n\nRAC is the cornerstone of Oracle's Maximum Availability Architecture (MAA) and is commonly combined with Data Guard for both HA within a site and DR across sites.\n\n---\n\n## 1. Core Architecture Components\n\n### Shared Storage\n\nAll RAC nodes access the same datafiles, control files, redo log groups, and archived logs. Oracle Automatic Storage Management (ASM) is the recommended storage layer. ASM provides:\n\n- Striping across disks for I/O throughput\n- Mirroring for local redundancy\n- ASM Cluster File System (ACFS) for non-database files\n\nEach node runs its own ASM instance (`+ASM1`, `+ASM2`, etc.) as a distinct Oracle instance type.\n\n### Oracle Clusterware (Grid Infrastructure)\n\nGrid Infrastructure (GI) is the cluster software stack that underpins RAC. It must be installed before RAC databases. Key GI components:\n\n| Component | Purpose |\n|---|---|\n| Oracle Cluster Registry (OCR) | Stores cluster topology, resource definitions, and configuration |\n| Voting Disk | Used for node eviction decisions during split-brain scenarios |\n| CSS (Cluster Synchronization Services) | Heartbeat and membership management |\n| CRS (Cluster Ready Services) | Resource management — starts/stops/monitors DB, VIPs, SCAN |\n| CTSS (Cluster Time Synchronization Service) | Keeps node clocks synchronized if NTP is not used |\n| SCAN (Single Client Access Name) | Single hostname resolving to 1–3 IPs for client connection load balancing (Oracle recommends 3) |\n\n### Instance Components Unique to RAC\n\nEach RAC instance has background processes beyond those found in single-instance Oracle:\n\n| Process | Description |\n|---|---|\n| LMS (Lock Manager Server) | Serves block transfer requests from other nodes; multiple LMS processes per instance |\n| LMD (Lock Manager Daemon) | Manages enqueue requests; communicates with remote LMDs |\n| LCK (Lock Process) | Handles instance locks (non-PCM) |\n| LMON (Global Enqueue Service Monitor) | Monitors cluster reconfiguration events; handles global enqueue recovery and instance recovery |\n| DIAG (Diagnosability Process) | Captures diagnostic data for global resource issues |\n| RMSn (RAC Management Server) | Manages Oracle resources in the cluster |\n\n---\n\n## 2. Cache Fusion and the Interconnect\n\nCache Fusion is Oracle's protocol for moving data blocks between instances over the private interconnect rather than writing them to and re-reading from disk. It is the defining technology that makes shared-disk RAC practical.\n\n### How Cache Fusion Works\n\n1. Instance 1 reads block 42 from disk into its buffer cache.\n2. Instance 2 needs block 42. Instead of a disk I/O, Oracle's Global Cache Service (GCS) identifies that Instance 1 holds block 42.\n3. GCS instructs Instance 1 to ship block 42 directly to Instance 2 over the private interconnect.\n4. Instance 2 receives the current version of the block without ever touching disk.\n\nThis block transfer is called a **CR (Consistent Read) transfer** when the receiver needs an older image for read consistency, or a **Current Block transfer** when the receiver needs the most recent committed version to modify it.\n\n### The Private Interconnect\n\nThe interconnect is a dedicated, low-latency, high-bandwidth network used exclusively for Cache Fusion traffic and cluster heartbeats. Requirements:\n\n- Must be a **private** network (never route interconnect traffic over public interfaces)\n- Target latency: sub-1ms round-trip\n- Bandwidth: 10 GbE minimum; 25 GbE or InfiniBand for high-throughput workloads\n- Redundancy: bonded NICs (active/passive or active/active) are strongly recommended\n\n```sql\n-- Verify interconnect configuration from GV$ views\nSELECT inst_id, name, ip_address, is_public\nFROM gv$cluster_interconnects\nORDER BY inst_id, name;\n\n-- Check interconnect statistics\nSELECT inst_id,\n SUM(CASE WHEN name = 'gc cr blocks received' THEN value ELSE 0 END) AS gc_cr_blocks_received,\n SUM(CASE WHEN name = 'gc current blocks received' THEN value ELSE 0 END) AS gc_current_blocks_received,\n SUM(CASE WHEN name = 'gc cr blocks served' THEN value ELSE 0 END) AS gc_cr_blocks_served,\n SUM(CASE WHEN name = 'gc current blocks served' THEN value ELSE 0 END) AS gc_current_blocks_served\nFROM gv$sysstat\nWHERE name IN ('gc cr blocks received', 'gc current blocks received',\n 'gc cr blocks served', 'gc current blocks served')\nGROUP BY inst_id\nORDER BY inst_id;\n```\n\n---\n\n## 3. Global Cache Service (GCS) and Global Enqueue Service (GES)\n\n### Global Cache Service (GCS)\n\nGCS manages the state of all data blocks across all instances. Each block has a master instance that tracks its state. The GCS maintains a distributed lock called a **PCM (Parallel Cache Management) lock** for every block.\n\nBlock states tracked by GCS:\n- **LOCAL** — owned and potentially modified by the local instance only\n- **SHARED** — multiple instances hold a consistent read copy\n- **NULL** — the instance no longer needs the block in current mode\n\n### Global Enqueue Service (GES)\n\nGES manages non-cache-fusion resources that span instances, such as dictionary locks, DML locks, sequences, and DDL locks. GES ensures that when Instance 1 acquires a lock on a row, Instance 2 cannot acquire a conflicting lock on the same row.\n\n### Monitoring GCS/GES Activity\n\n```sql\n-- Global cache efficiency: ratio of disk reads avoided by Cache Fusion\nSELECT inst_id,\n ROUND(\n (gc_cr_blocks_received + gc_current_blocks_received) /\n NULLIF(physical_reads + gc_cr_blocks_received + gc_current_blocks_received, 0) * 100,\n 2\n ) AS cache_fusion_pct\nFROM (\n SELECT inst_id,\n SUM(CASE WHEN name = 'gc cr blocks received' THEN value ELSE 0 END) AS gc_cr_blocks_received,\n SUM(CASE WHEN name = 'gc current blocks received' THEN value ELSE 0 END) AS gc_current_blocks_received,\n SUM(CASE WHEN name = 'physical reads' THEN value ELSE 0 END) AS physical_reads\n FROM gv$sysstat\n WHERE name IN ('gc cr blocks received', 'gc current blocks received', 'physical reads')\n GROUP BY inst_id\n)\nORDER BY inst_id;\n\n-- Top segments causing cross-instance block transfers\nSELECT inst_id,\n owner,\n object_name,\n object_type,\n SUM(CASE WHEN statistic_name = 'gc buffer busy waits' THEN value ELSE 0 END) AS gc_buffer_busy_waits,\n SUM(CASE WHEN statistic_name = 'gc cr blocks received' THEN value ELSE 0 END) AS gc_cr_blocks_received,\n SUM(CASE WHEN statistic_name = 'gc current blocks received' THEN value ELSE 0 END) AS gc_current_blocks_received\nFROM gv$segment_statistics\nWHERE statistic_name IN ('gc buffer busy waits', 'gc cr blocks received', 'gc current blocks received')\nGROUP BY inst_id, owner, object_name, object_type\nHAVING SUM(CASE WHEN statistic_name = 'gc buffer busy waits' THEN value ELSE 0 END) > 0\nORDER BY gc_buffer_busy_waits DESC\nFETCH FIRST 20 ROWS ONLY;\n```\n\n---\n\n## 4. RAC Services Configuration\n\nRAC services are the primary mechanism for directing workload to specific nodes and enabling transparent failover. A service is a named entity that clients connect to rather than connecting directly to an instance.\n\n### Service Types\n\n| Service Type | Description |\n|---|---|\n| Preferred/Available | Service runs on preferred node(s); fails over to available node(s) on failure |\n| Uniform | Service runs on all nodes simultaneously |\n| Administrator-managed | You define preferred/available instances manually |\n| Policy-managed (Server Pools) | Oracle GI manages instance counts dynamically based on server pool policies |\n\n### Creating and Configuring Services\n\n```sql\n-- Using DBCA or SRVCTL (preferred for RAC services)\n-- From the OS command line on a cluster node:\n--\n-- Create a service with preferred instances\n-- srvctl add service -db MYDB -service OLTP_SVC \\\n-- -preferred MYDB1,MYDB2 -available MYDB3\n\n-- Verify service configuration\n-- srvctl config service -db MYDB -service OLTP_SVC\n\n-- Start/stop a service\n-- srvctl start service -db MYDB -service OLTP_SVC\n-- srvctl stop service -db MYDB -service OLTP_SVC\n\n-- From within SQL*Plus: create a service programmatically\nBEGIN\n DBMS_SERVICE.CREATE_SERVICE(\n service_name => 'OLTP_SVC',\n network_name => 'OLTP_SVC',\n goal => DBMS_SERVICE.GOAL_THROUGHPUT,\n clb_goal => DBMS_SERVICE.CLB_GOAL_LONG\n );\nEND;\n/\n\n-- Check active services on each instance\nSELECT inst_id, name, network_name, goal, clb_goal\nFROM gv$active_services\nWHERE name NOT IN ('SYS$BACKGROUND', 'SYS$USERS')\nORDER BY name, inst_id;\n```\n\n### Service Attributes for Performance\n\n```sql\n-- Service-level thresholds trigger alerts when violated\nBEGIN\n DBMS_SERVICE.MODIFY_SERVICE(\n service_name => 'OLTP_SVC',\n goal => DBMS_SERVICE.GOAL_SERVICE_TIME,\n clb_goal => DBMS_SERVICE.CLB_GOAL_SHORT,\n -- Alert when elapsed time per call exceeds 5 seconds\n aq_ha_notifications => TRUE,\n -- Commit outcome tracking (for at-most-once execution)\n commit_outcome => TRUE,\n retention_timeout => 604800 -- 7 days in seconds\n );\nEND;\n/\n```\n\n---\n\n## 5. Node Affinity\n\nNode affinity is the practice of binding specific workloads or schemas to specific nodes in the cluster. This is important for reducing Cache Fusion traffic: when the same data is always accessed by the same node, there are no cross-instance block transfers.\n\n### Application-Level Affinity\n\nThe most reliable form of affinity is achieved by routing application connections through dedicated services bound to specific instances:\n\n```\nOLTP Application -> OLTP_SVC -> Node 1 & 2 (preferred)\nReport Application -> RPT_SVC -> Node 3 & 4 (preferred)\nBatch Jobs -> BATCH_SVC -> Node 4 (preferred)\n```\n\nWith this topology, OLTP data blocks live primarily in Node 1/2 buffer caches and reporting data lives primarily in Node 3/4 caches, minimizing cross-instance transfers.\n\n### Table Partitioning and Affinity\n\nIn policy-managed databases with multiple server pools, partitioned tables can be used so that different partitions are predominantly accessed by different node sets. This is called **partition affinity**.\n\n```sql\n-- Example: Range-partitioned sales table where regional apps connect\n-- through region-specific services mapped to specific nodes\nCREATE TABLE SALES (\n sale_id NUMBER NOT NULL,\n region_id NUMBER(2) NOT NULL,\n sale_date DATE NOT NULL,\n amount NUMBER(12,2) NOT NULL\n)\nPARTITION BY RANGE (region_id) (\n PARTITION sales_region_1 VALUES LESS THAN (6), -- Nodes 1-2\n PARTITION sales_region_2 VALUES LESS THAN (11), -- Nodes 3-4\n PARTITION sales_region_3 VALUES LESS THAN (16), -- Nodes 5-6\n PARTITION sales_other VALUES LESS THAN (MAXVALUE)\n);\n```\n\n---\n\n## 6. RAC-Specific Wait Events\n\nRAC introduces a set of wait events that do not exist in single-instance Oracle. High wait times on these events indicate interconnect or cache contention issues.\n\n### Critical Wait Events\n\n| Wait Event | Cause | Threshold |\n|---|---|---|\n| `gc buffer busy acquire` | Local session waiting to acquire a block being shipped from remote | \u003c 1ms avg |\n| `gc buffer busy release` | Local session waiting while another local session holds a block being requested remotely | \u003c 1ms avg |\n| `gc cr block busy` | Requesting a CR copy of a block while the master is processing a transfer | \u003c 2ms avg |\n| `gc current block busy` | Requesting current block; remote instance has not yet shipped it | \u003c 2ms avg |\n| `gc cr block 2-way` | Normal 2-way CR block transfer (requester + master); acceptable baseline | \u003c 1ms |\n| `gc current block 2-way` | Normal 2-way current block transfer | \u003c 1ms |\n| `gc cr block 3-way` | 3-way transfer (requester + master + holder); higher latency | \u003c 2ms |\n| `gc current block 3-way` | 3-way current block transfer | \u003c 2ms |\n| `gcs log flush sync` | Waiting for remote instance to flush its redo log before block transfer | Check redo |\n| `enq: TX - row lock contention` | Row-level lock held on another instance | Application issue |\n\n```sql\n-- Top RAC wait events by total wait time\nSELECT inst_id,\n event,\n total_waits,\n time_waited_micro / 1e6 AS total_sec,\n ROUND(time_waited_micro / NULLIF(total_waits, 0) / 1000, 3) AS avg_wait_ms\nFROM gv$system_event\nWHERE event LIKE 'gc %'\n OR event LIKE 'gcs %'\n OR event LIKE 'ges %'\nORDER BY time_waited_micro DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- Session-level RAC waits (for diagnosing a specific connection)\nSELECT sid, event, state, wait_class,\n seconds_in_wait, p1, p2, p3\nFROM gv$session\nWHERE wait_class != 'Idle'\n AND (event LIKE 'gc %' OR event LIKE 'gcs %')\nORDER BY seconds_in_wait DESC;\n```\n\n---\n\n## 7. Transparent Application Failover (TAF) and Fast Connection Failover (FCF)\n\n### Transparent Application Failover (TAF)\n\nTAF is a client-side failover mechanism configured in the TNS descriptor or Oracle Connection Pool. When the instance a client is connected to fails, TAF automatically reconnects the client to a surviving instance and optionally replays the current SELECT statement from the point of failure.\n\n```\n# TNS entry with TAF (tnsnames.ora)\nMYDB_TAF =\n (DESCRIPTION =\n (FAILOVER = ON)\n (LOAD_BALANCE = OFF)\n (ADDRESS = (PROTOCOL = TCP)(HOST = node1-vip)(PORT = 1521))\n (ADDRESS = (PROTOCOL = TCP)(HOST = node2-vip)(PORT = 1521))\n (CONNECT_DATA =\n (SERVICE_NAME = OLTP_SVC)\n (FAILOVER_MODE =\n (TYPE = SELECT) -- or SESSION for non-query failover\n (METHOD = BASIC) -- or PRECONNECT for pre-established shadow connection\n (RETRIES = 30)\n (DELAY = 5)\n )\n )\n )\n```\n\nTAF limitations:\n- In-progress DML is **not** replayed; the application receives an error for uncommitted transactions\n- `TYPE=SELECT` re-executes the query from the beginning (rows already fetched are skipped)\n- TAF does not protect against network partitions mid-transaction\n\n### Fast Connection Failover (FCF)\n\nFCF uses Oracle Notification Service (ONS) and is configured in JDBC Thin drivers or Universal Connection Pool (UCP). FCF is superior to TAF for Java applications:\n\n- The connection pool receives an ONS event (via the GI event notification system) immediately when a service goes down\n- Stale connections are proactively removed from the pool before applications try to use them\n- Works with Application Continuity (AC) for transparent replay of in-flight transactions\n\n```java\n// UCP configuration for FCF (conceptual — not SQL)\n// dataSource.setFastConnectionFailoverEnabled(true);\n// dataSource.setONSConfiguration(\"nodes=node1:6200,node2:6200\");\n```\n\n### Application Continuity (AC) and Transparent Application Continuity (TAC)\n\nApplication Continuity (introduced in Oracle Database 12c Release 1) extends TAF concepts to transparently replay in-flight transactions, including DML, after a recoverable error. TAC (introduced in 18c, expanded in 19c) does this without any application configuration by using `failover_type => 'AUTO'` on the service (rather than `'TRANSACTION'` for standard AC).\n\n```sql\n-- Check if Application Continuity is enabled for a service\nSELECT name, failover_type, failover_method, goal, commit_outcome, retention_timeout\nFROM dba_services\nWHERE name = 'OLTP_SVC';\n\n-- Enable Application Continuity on a service (failover_type => 'TRANSACTION' = AC)\n-- For Transparent Application Continuity (TAC, 18c+), use failover_type => 'AUTO'\nBEGIN\n DBMS_SERVICE.MODIFY_SERVICE(\n service_name => 'OLTP_SVC',\n failover_type => 'TRANSACTION', -- enables AC; use 'AUTO' for TAC (18c+)\n commit_outcome => TRUE,\n retention_timeout => 86400\n );\nEND;\n/\n```\n\n---\n\n## 8. Cluster Verification Utility (CVU)\n\nCVU (`cluvfy`) is Oracle's pre-installation and post-installation diagnostic tool for cluster environments. It checks network configuration, OS parameters, shared storage, and cluster software health.\n\n```bash\n# Pre-installation checks (run before installing Grid Infrastructure)\n# cluvfy stage -pre crsinst -n node1,node2 -verbose\n\n# Post-installation check\n# cluvfy stage -post crsinst -n node1,node2\n\n# Verify the cluster at any time\n# cluvfy comp sys -n node1,node2 -- OS parameters\n# cluvfy comp nodecon -n node1,node2 -- node connectivity\n# cluvfy comp ocr -n node1,node2 -- OCR integrity\n# cluvfy comp ssa -n node1,node2 -s disk_group_name -- shared storage\n\n-- Verify RAC database health from SQL*Plus\n-- Check all instances are open\nSELECT inst_id, instance_name, host_name, status, database_status\nFROM gv$instance\nORDER BY inst_id;\n\n-- Check cluster_interconnects\nSELECT inst_id, name, ip_address, is_public, source\nFROM gv$cluster_interconnects;\n\n-- Verify all datafiles are accessible from all instances\nSELECT inst_id, file#, status, name\nFROM gv$datafile_header\nWHERE status != 'ONLINE'\nORDER BY inst_id, file#;\n\n-- Check voting disk and OCR status (from OS as root)\n-- crsctl query css votedisk\n-- ocrcheck\n```\n\n---\n\n## 9. Best Practices\n\n- **Use SCAN (Single Client Access Name) for all client connections.** SCAN provides a single address for clients regardless of how many nodes exist. Never hardcode VIP addresses in application connection strings.\n- **Run Grid Infrastructure and Database homes on separate file systems.** GI patching and DB patching are independent; separate homes prevent unintended outages.\n- **Size the interconnect for peak block transfer load.** Monitor `gc cr blocks received` and `gc current blocks received` rates to project bandwidth requirements. A saturated interconnect is the leading cause of RAC performance degradation.\n- **Design services before deployment.** Services should map 1:1 to application workload types (OLTP, reporting, batch). Mixing workloads in one service makes diagnosis and isolation impossible.\n- **Never use `ALTER SYSTEM` to set initialization parameters in RAC without specifying `SID=*` or `SID=\u003csid>`.** A misconfigured parameter on one instance can cause that instance to crash.\n- **Prefer policy-managed databases for new deployments.** Server pools allow Oracle to dynamically rebalance instances across nodes during node failure without manual intervention.\n- **Enable Cluster Health Monitor (CHM/OS Watcher).** CHM captures OS-level metrics (CPU, memory, network, disk I/O) per-node with 1-second granularity, which is invaluable during post-mortem analysis of node evictions.\n\n---\n\n## 10. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Routing Interconnect Traffic Over the Public Network\n\nIf `/etc/hosts` or DNS does not have a proper private hostname entry, Oracle may default to using the public network for interconnect traffic. This floods the public NIC and causes massive GCS latency.\n\n**Fix:** Always verify the interconnect IP is on the private network before and after installation.\n\n```sql\n-- Confirm interconnect is NOT the public IP\nSELECT name, ip_address, is_public FROM gv$cluster_interconnects;\n-- is_public should be 'NO' for the active interconnect\n```\n\n### Mistake 2: Using Default UNDO and TEMP Tablespaces Shared Across Instances\n\nRAC requires one UNDO tablespace **per instance**. Using a single UNDO tablespace is not supported in RAC.\n\n```sql\n-- Check UNDO tablespace assignment per instance\nSELECT inst_id, value AS undo_tablespace\nFROM gv$parameter\nWHERE name = 'undo_tablespace'\nORDER BY inst_id;\n\n-- Each inst_id should have a unique undo tablespace name\n-- UNDOTBS1 -> inst 1, UNDOTBS2 -> inst 2, etc.\n```\n\n### Mistake 3: Not Indexing Sequence-Driven Primary Keys for Cache Fusion\n\nWith a single sequence shared across all instances, the \"right-hand side\" index leaf block becomes a hot block that every instance races to update. This causes massive `gc buffer busy` waits.\n\n**Fix:** Use a **reverse key index** or hash partitioned index on sequence-generated keys in high-concurrency RAC insert scenarios.\n\n```sql\n-- Reverse key index reduces right-hand contention\nCREATE INDEX IX_ORDERS_ORDER_ID ON ORDERS (order_id) REVERSE;\n\n-- Alternatively, use a sequence with a large CACHE value to reduce SGA pressure\nCREATE SEQUENCE SEQ_ORDERS\n START WITH 1\n INCREMENT BY 1\n CACHE 1000 -- Each instance pre-allocates 1000 values; reduces GES traffic\n NOORDER; -- NOORDER avoids ordering overhead in RAC; fine for surrogate keys\n```\n\n### Mistake 4: Ignoring Node Eviction Root Causes\n\nNode evictions (where a hung or slow node is forcibly removed from the cluster) are often treated as hardware failures when they are actually caused by:\n- A frozen OS process holding a cluster lock\n- A slow interconnect causing missed heartbeats\n- An overloaded node failing to respond to CSS within the `misscount` timeout\n\nAlways analyze `cssd.log`, `alert_\u003csid>.log`, and CHM data together before concluding hardware is at fault.\n\n### Mistake 5: Applying Patches Without Running `opatchauto`\n\nIn a RAC environment, GI and RAC patches must be applied with `opatchauto` in a rolling manner. Manually applying patches without `opatchauto` can leave GI and DB homes in an inconsistent state across nodes, leading to split-brain scenarios.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Real Application Clusters Administration and Deployment Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/racad/) — RAC architecture, background processes, GCS/GES, services, TAF, CVU\n- [Oracle RAC 19c Glossary](https://docs.oracle.com/en/database/oracle/oracle-database/19/racad/glossary.html) — LMON (Global Enqueue Service Monitor), LMSn, LMD, Cache Fusion, GCS, GES, SCAN, OCR, CSS, CTSS definitions\n- [About RAC Background Processes (12c doc, applies to 19c)](https://docs.oracle.com/database/121/RACAD/GUID-AEBD3F49-4F10-4BDE-9008-DC1AF8E7DB42.htm) — LMS, LMD, LCK, LMON, DIAG, RMSn descriptions\n- [About Connecting to an Oracle RAC Database Using SCANs](https://docs.oracle.com/en/database/oracle/oracle-database/19/rilin/about-connecting-to-an-oracle-rac-database-using-scans.html) — SCAN resolves to 1–3 IPs; Oracle recommends 3\n- [Ensuring Application Continuity (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/racad/ensuring-application-continuity.html) — AC introduced in 12c R1; TAC introduced in 18c; FAILOVER_TYPE values\n- [DBMS_SERVICE (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_SERVICE.html) — failover_type valid values (TRANSACTION, SELECT, SESSION, NONE)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":23604,"content_sha256":"080afd11eef6654cf87696cfaf70c15344d0ca6f470667f0619e1e9dd892e477"},{"filename":"containers/adb-free.md","content":"# `adb-free` OCR Repository\n\n## Overview\n\n`database/adb-free` is the Oracle Container Registry repository for Oracle Autonomous Database Free. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/adb-free`\n- **OCR short description:** Oracle Autonomous Database Free\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/adb-free:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Oracle Free Use Terms and Conditions provided in the container image.\n\n## What Oracle Documents Here\n\n- The OCR readme says Oracle Autonomous Database Free supports two workload types: `ADW` and `ATP`.\n- The page includes a version matrix showing `latest-23ai` for the 23ai line and `latest` for the 19c line, with specific release tags alongside each stream.\n- OCR also documents container resource requirements of 4 CPUs and 8 GiB memory.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page provides an explicit version matrix: the 23ai stream uses `latest-23ai`, while the 19c stream uses `latest`. Use that matrix instead of assuming a single default line.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need Autonomous Database Free container workflows (ADW/ATP modes).\n- **Use another image when:** Avoid when a generic Database Free runtime is enough; use free.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/adb-free:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/adb-free:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/adb-free\n- https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/autonomous-docker-container.html#GUID-03B5601E-E15B-4ECC-9929-D06ACF576857\n- https://www.oracle.com/downloads/licenses/oracle-free-license.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2429,"content_sha256":"585552aae246351979bf0da8d0d5c040f3af963ebb7213c5fac5a4dc04b47f53"},{"filename":"containers/cman.md","content":"# `cman` OCR Repository\n\n## Overview\n\n`database/cman` is the Oracle Container Registry repository for Oracle Connection Manager. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/cman`\n- **OCR short description:** Oracle Connection Manager\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/cman:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme positions this image as Oracle Connection Manager in Linux containers for proxying and managing client database connections.\n- The page says you can use the container with Oracle RAC or a single-instance Oracle Database, as long as the SCAN name or database hostname is resolvable from the container.\n- The OCR readme examples currently use `container-registry.oracle.com/database/client-cman:latest`, while the repository pull-command section is published under `container-registry.oracle.com/database/cman:latest`.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page does not publish a dedicated 19c-versus-26ai compatibility matrix for `cman`. Use the OCR tags table on the repository page to choose the image version you need.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need Oracle Connection Manager as a proxy/gateway layer.\n- **Use another image when:** Avoid when clients can connect directly and no proxy tier is required.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/cman:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/cman:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/cman\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2405,"content_sha256":"98be7328f572eca3206e157d64b13e2855f266e42258fcd07b801f4eb0e368e6"},{"filename":"containers/container-selection-matrix.md","content":"# Oracle OCR Database Containers: Selection Matrix\n\n## Overview\n\nUse this guide to quickly choose the right Oracle Container Registry (OCR) database-category image before drilling into a repo-specific skill file.\n\n## Quick Decision Matrix\n\n| Repository | Choose This When | Avoid / Use Alternative When | What It Offers |\n|---|---|---|---|\n| `database/enterprise` | You need Oracle AI Database 26ai Enterprise Edition in a container | You need CPU RU stream images for patch-level pinning (`enterprise_ru`) | Full Enterprise server image, multitenant default setup |\n| `database/enterprise_ru` | You need CPU repository / RU-tagged Enterprise images (commonly 19c stream) | You want latest non-CPU stream (`enterprise`) | RU-tagged Enterprise server images under CPU terms |\n| `database/free` | You want Oracle AI Database 26ai Free for local development/learning | You need paid Enterprise capabilities | Fast-start Oracle Database Free server image |\n| `database/adb-free` | You need Autonomous Database Free container workflows (`ADW`/`ATP`) | You just need generic Database Free (`free`) | Autonomous DB Free image with workload modes and version matrix |\n| `database/rac` | You need Oracle RAC in containers (Podman-focused guidance) | You need RU-tagged RAC stream (`rac_ru`) | RAC container deployment line with production-focused docs |\n| `database/rac_ru` | You need CPU/RU-tagged RAC images | You want non-RU RAC stream (`rac`) | RAC release-update image stream under CPU terms |\n| `database/gsm` | You are deploying Oracle Globally Distributed Database and need GSM | You need RU-tagged GSM stream (`gsm_ru`) | Global Service Manager container for GDD setups |\n| `database/gsm_ru` | You need CPU/RU-tagged GSM images | You want non-RU GSM stream (`gsm`) | GSM RU stream for GDD container topologies |\n| `database/cman` | You need Oracle Connection Manager as a proxy/gateway for DB clients | You only need direct client-to-DB connectivity | Connection brokering/proxy image for DB network paths |\n| `database/instantclient` | You need client-only tooling/libraries (OCI/OCCI/SQL*Plus) in containers | You need a full database server container | Instant Client packages (Basic/SDK/SQL*Plus) |\n| `database/sqlcl` | You need SQLcl CLI in containerized automation or CI jobs | You need ORDS or database server runtime | SQL Command Line image with scriptable CLI workflows |\n| `database/ords` | You need supported ORDS container CLI/runtime image | You need a non-deprecated ORDS image line for new deployments | Current ORDS container image line |\n| `database/operator` | You run Oracle DB lifecycle through Kubernetes operator patterns | You are not using Kubernetes operator model | Oracle Database Operator image for Kubernetes automation |\n| `database/observability-exporter` | You need metrics/logs/traces export for Oracle DB observability | You need a DB server image | OpenTelemetry-style exporter tooling image |\n| `database/private-ai` | You need Oracle Private AI Services container deployment | You only need base database without Private AI service layer | Private AI service runtime with secure/non-secure setup modes |\n| `database/graph-quickstart` | You want a fast Property Graph 26ai learning sandbox | You need production-grade graph deployment | Quickstart graph-focused image built on 26ai Free |\n| `database/otmm` | You need free Oracle Transaction Manager for Microservices workflows | You need Enterprise edition features (`microtx-ee-coordinator`) | MicroTx Free image for distributed transaction patterns |\n| `database/microtx-ee-coordinator` | You need MicroTx Enterprise coordinator features | You only need free variant (`otmm`) | Enterprise coordinator for XA/Saga/TCC patterns |\n| `database/microtx-ee-console` | You need MicroTx Enterprise web console/monitoring | You are not running MicroTx EE coordinator | Console UI for MicroTx EE operations |\n\n## Minimal Pull and Run Pattern\n\n1. Sign in and accept repository terms on OCR for the target image.\n2. Pull the image with an explicit tag when possible.\n3. Start with the OCR README sample command for that repository.\n\n```bash\ndocker login container-registry.oracle.com\n\ndocker pull container-registry.oracle.com/database/\u003crepo>:\u003ctag>\n\ndocker run --name \u003cname> --rm -it container-registry.oracle.com/database/\u003crepo>:\u003ctag>\n```\n\nFor database server images, add required environment variables, persistent volumes, and published ports exactly as documented on the repo page before production use.\n\n## Oracle Version Notes (19c vs 26ai)\n\nOCR container repositories are split across product lines, not a single unified 19c-vs-26ai matrix:\n\n- 26ai-oriented images include repositories such as `enterprise`, `free`, and `graph-quickstart`.\n- 19c/RU-oriented streams are surfaced in repositories such as `enterprise_ru` and `rac_ru`.\n- Tooling/runtime images (`sqlcl`, `ords`, `operator`, `observability-exporter`, `instantclient`) use their own release tags and should be pinned by image tag.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database\n- https://container-registry.oracle.com/ords/ocr/ba/database/enterprise\n- https://container-registry.oracle.com/ords/ocr/ba/database/enterprise_ru\n- https://container-registry.oracle.com/ords/ocr/ba/database/free\n- https://container-registry.oracle.com/ords/ocr/ba/database/adb-free\n- https://container-registry.oracle.com/ords/ocr/ba/database/rac\n- https://container-registry.oracle.com/ords/ocr/ba/database/rac_ru\n- https://container-registry.oracle.com/ords/ocr/ba/database/gsm\n- https://container-registry.oracle.com/ords/ocr/ba/database/gsm_ru\n- https://container-registry.oracle.com/ords/ocr/ba/database/cman\n- https://container-registry.oracle.com/ords/ocr/ba/database/instantclient\n- https://container-registry.oracle.com/ords/ocr/ba/database/sqlcl\n- https://container-registry.oracle.com/ords/ocr/ba/database/ords\n- https://container-registry.oracle.com/ords/ocr/ba/database/operator\n- https://container-registry.oracle.com/ords/ocr/ba/database/observability-exporter\n- https://container-registry.oracle.com/ords/ocr/ba/database/private-ai\n- https://container-registry.oracle.com/ords/ocr/ba/database/graph-quickstart\n- https://container-registry.oracle.com/ords/ocr/ba/database/otmm\n- https://container-registry.oracle.com/ords/ocr/ba/database/microtx-ee-coordinator\n- https://container-registry.oracle.com/ords/ocr/ba/database/microtx-ee-console\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6408,"content_sha256":"66e696b92c6b93f644fbb6387ffce8664b627e966e850efcfff855a7730f8234"},{"filename":"containers/enterprise_ru.md","content":"# `enterprise_ru` OCR Repository\n\n## Overview\n\n`database/enterprise_ru` is the Oracle Container Registry repository for Oracle Database Enterprise Edition. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/enterprise_ru`\n- **OCR short description:** Oracle Database Enterprise Edition\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/enterprise_ru:latest-19`\n- **License note on OCR:** OCR states that you must accept the Oracle Container Registry Critical Patch Update (CPU) Repository Terms and Restrictions before downloading from this repository.\n\n## What Oracle Documents Here\n\n- The OCR readme documents this repository as Oracle Database Server Release Update 19c Docker image documentation.\n- The page says the image runs on Oracle Linux 7 and contains a default multitenant database with one pluggable database.\n- OCR covers startup, connections, patching the existing database, and SGA/PGA sizing for this Release Update image line.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page explicitly documents this repository as the 19c Release Update image line, and the latest OCR pull command currently uses the `latest-19` tag stream.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need RU-tagged Enterprise images from the CPU repository stream.\n- **Use another image when:** Avoid when you want the non-CPU latest stream; use enterprise instead.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/enterprise_ru:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/enterprise_ru:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/enterprise_ru\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2290,"content_sha256":"e7fce40b3297269b19166fa94eec603f228c3a34da912465bd2497c6523f611d"},{"filename":"containers/enterprise.md","content":"# `enterprise` OCR Repository\n\n## Overview\n\n`database/enterprise` is the Oracle Container Registry repository for Oracle Database Enterprise Edition. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/enterprise`\n- **OCR short description:** Oracle Database Enterprise Edition\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/enterprise:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme documents this image as Oracle AI Database Server Release 26ai Enterprise Edition running on Oracle Linux 8.\n- The page says the image contains a default database in a multitenant configuration with one pluggable database.\n- The OCR documentation covers startup, connections, data-volume reuse, and SGA/PGA sizing with `INIT_SGA_SIZE` and `INIT_PGA_SIZE`.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page explicitly documents `enterprise` as the 26ai Enterprise Edition server image. OCR publishes `enterprise_ru` separately for Release Update container images.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need Oracle AI Database 26ai Enterprise Edition in a container.\n- **Use another image when:** Avoid when you need CPU/RU stream patch pinning; use enterprise_ru instead.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/enterprise:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/enterprise:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/enterprise\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2311,"content_sha256":"f7271e59cd9b67d1958fa2bf52a6a8a6e2de6a7973255283a842ba20806cfb24"},{"filename":"containers/free.md","content":"# `free` OCR Repository\n\n## Overview\n\n`database/free` is the Oracle Container Registry repository for Oracle Database Free. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/free`\n- **OCR short description:** Oracle Database Free\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/free:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Oracle Free Use Terms and Conditions provided in the container image.\n\n## What Oracle Documents Here\n\n- The OCR readme documents `free` as Oracle AI Database 26ai Free on top of an Oracle Linux 8 base image.\n- The page says the image contains a pre-built database for fast startup and is helpful in CI/CD scenarios.\n- OCR also notes that Oracle Enterprise Manager Database Express is no longer supported with Oracle AI Database 26ai Free and recommends SQL Developer instead.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page explicitly documents `free` as the Oracle AI Database 26ai Free container image. For a separate 19c line in the Autonomous Database Free family, OCR publishes `adb-free` with its own version matrix.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use for local development and learning with Oracle AI Database 26ai Free.\n- **Use another image when:** Avoid when you need Enterprise-only features or support commitments.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/free:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/free:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/free\n- https://www.oracle.com/database/free/\n- https://www.oracle.com/database/sqldeveloper/\n- https://www.oracle.com/downloads/licenses/oracle-free-license.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2385,"content_sha256":"05a68981a2c3383e984eabdb88687f980d7e6a6cf5e793b71094cbd44912e74a"},{"filename":"containers/graph-quickstart.md","content":"# `graph-quickstart` OCR Repository\n\n## Overview\n\n`database/graph-quickstart` is the Oracle Container Registry repository for Get started with the Property Graph feature of Oracle AI Database 26ai. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/graph-quickstart`\n- **OCR short description:** Get started with the Property Graph feature of Oracle AI Database 26ai\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/graph-quickstart:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme describes this as the Oracle Graph Quickstart container image for the Property Graph feature of Oracle AI Database 26ai.\n- The page says the image includes Oracle AI Database 26ai Free, a preconfigured `GRAPHUSER`, and a sample SQL Property Graph.\n- OCR also notes that this image is not for production workloads, and that it is based on the Oracle AI Database 26ai Free container image (Lite).\n\n## Oracle Version Notes (19c vs 26ai)\n\nThis repository is explicitly tied to Oracle AI Database 26ai. The OCR readme says the image is based on the 26ai Free container image and should not be used for production workloads.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use for quick Property Graph exploration and demos on 26ai.\n- **Use another image when:** Avoid for production workloads; use supported production deployment patterns.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/graph-quickstart:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/graph-quickstart:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/graph-quickstart\n- https://docs.oracle.com/en/database/oracle/property-graph/index.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2527,"content_sha256":"403e302d487bc573309501d88c8049da0bde4c8f9b38b81a2fceee21522f5a91"},{"filename":"containers/gsm_ru.md","content":"# `gsm_ru` OCR Repository\n\n## Overview\n\n`database/gsm_ru` is the Oracle Container Registry repository for Oracle Global Service Manager. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/gsm_ru`\n- **OCR short description:** Oracle Global Service Manager\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/gsm_ru:latest-23`\n- **License note on OCR:** OCR states that you must accept the Oracle Container Registry Critical Patch Update (CPU) Repository Terms and Restrictions before downloading from this repository.\n\n## What Oracle Documents Here\n\n- The OCR readme positions `gsm_ru` as the Global Service Manager image for Oracle Globally Distributed Database in the CPU repository stream.\n- The page says the GSM container is required to configure Oracle Globally Distributed Database and shows it alongside Release Update database images.\n- OCR documents Podman installation, bridge setup, host-file preparation, and container deployment steps for this repository.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page does not publish a separate 19c-versus-26ai matrix for `gsm_ru`, but the repository is under CPU repository terms and the latest OCR pull command currently uses the `latest-23` tag stream.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need RU-tagged GSM images under CPU terms.\n- **Use another image when:** Avoid when non-RU GSM stream is sufficient; use gsm.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/gsm_ru:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/gsm_ru:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/gsm_ru\n- https://docs.oracle.com/en/operating-systems/oracle-linux/podman/toc.htm\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2365,"content_sha256":"af640a7ad43ac50760f7ec271903a9d8f8b18bb91d6a6ed619ca565f2ef21c59"},{"filename":"containers/gsm.md","content":"# `gsm` OCR Repository\n\n## Overview\n\n`database/gsm` is the Oracle Container Registry repository for Oracle Global Service Manager. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/gsm`\n- **OCR short description:** Oracle Global Service Manager\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/gsm:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme documents `gsm` as the Oracle Global Service Manager container for Oracle Globally Distributed Database on container.\n- The page says the GSM container is required to configure Oracle Globally Distributed Database.\n- The OCR documentation walks through Podman installation, network creation, host-file setup, and catalog-container deployment.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page does not provide a dedicated 19c-versus-26ai matrix for `gsm`. Use the OCR tags table for the exact image version, or `gsm_ru` if you need the CPU repository stream.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when deploying Oracle Globally Distributed Database and GSM is required.\n- **Use another image when:** Avoid when you need RU stream tagging; use gsm_ru.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/gsm:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/gsm:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/gsm\n- https://docs.oracle.com/en/operating-systems/oracle-linux/podman/toc.htm\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2298,"content_sha256":"befff3b7ac124450acef019f6331d4e912d5975e7ce276924f4c06c89216b6f3"},{"filename":"containers/instantclient.md","content":"# `instantclient` OCR Repository\n\n## Overview\n\n`database/instantclient` is the Oracle Container Registry repository for Oracle Instant Client. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/instantclient`\n- **OCR short description:** Oracle Instant Client\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/instantclient:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme says this image contains the Oracle Instant Client Basic, SDK, and SQL*Plus packages.\n- The page says you can extend the image to run OCI, OCCI, and JDBC applications, or scripting-language drivers that use OCI.\n- The OCR detail page includes both `latest` and versioned tags for selecting a specific Instant Client release.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page does not publish a dedicated 19c-versus-26ai matrix for `instantclient`. Use the OCR tags table to pick the client version that matches your target environment.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need Oracle client libraries/tools in a container, not a DB server.\n- **Use another image when:** Avoid when you need database instance startup; use free/enterprise/etc.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/instantclient:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/instantclient:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/instantclient\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2279,"content_sha256":"ca9e5aabbc075a3300504c25c8bfb724369388a1841ee1ece8365cbe595c349f"},{"filename":"containers/microtx-ee-console.md","content":"# `microtx-ee-console` OCR Repository\n\n## Overview\n\n`database/microtx-ee-console` is the Oracle Container Registry repository for Oracle Transaction Manager for Microservices(MicroTx) Console. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/microtx-ee-console`\n- **OCR short description:** Oracle Transaction Manager for Microservices(MicroTx) Console\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/microtx-ee-console:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme describes this image as the graphical web console for Oracle Transaction Manager for Microservices (MicroTx).\n- The page says the console is used to manage and monitor transactions.\n- OCR also says you can use the console only with MicroTx Enterprise Edition.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page focuses on MicroTx release tags rather than a 19c-versus-26ai database matrix. Use the repository tags table to select the console version you need.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need the MicroTx Enterprise console UI for operations/monitoring.\n- **Use another image when:** Avoid when you are not running MicroTx Enterprise coordinator workflows.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/microtx-ee-console:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/microtx-ee-console:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/microtx-ee-console\n- https://docs.oracle.com/pls/topic/lookup?ctx=microtx-latest&id=TMMDG-GUID-929926DE-384E-4426-9CBC-1B16940BE25C\n- https://docs.oracle.com/pls/topic/lookup?ctx=microtx-latest&id=TMMDG-GUID-F6ED47D2-97FE-481E-A41E-C320A3611C0B\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2542,"content_sha256":"338329a01c13247d8c705f103fc74232483923bba900aecc05674498ec95021c"},{"filename":"containers/microtx-ee-coordinator.md","content":"# `microtx-ee-coordinator` OCR Repository\n\n## Overview\n\n`database/microtx-ee-coordinator` is the Oracle Container Registry repository for Oracle Transaction Manager for Microservices (MicroTx) Enterprise Edition. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/microtx-ee-coordinator`\n- **OCR short description:** Oracle Transaction Manager for Microservices (MicroTx) Enterprise Edition\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/microtx-ee-coordinator:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme says MicroTx Enterprise Edition helps maintain transaction consistency across distributed microservices applications.\n- The page explicitly lists XA, Saga based on Eclipse MicroProfile Long Running Action (LRA), and Try-Confirm/Cancel (TCC) as supported distributed-transaction protocols.\n- OCR also says the Enterprise Edition adds features such as transaction promotion, metrics visualization, and use of the MicroTx console.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page tracks MicroTx image releases rather than a 19c-versus-26ai database matrix. Use the repository tags table to choose the coordinator version you need.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need MicroTx Enterprise coordinator features (XA/Saga/TCC).\n- **Use another image when:** Avoid when free-tier capabilities are sufficient; use otmm.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/microtx-ee-coordinator:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/microtx-ee-coordinator:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/microtx-ee-coordinator\n- https://docs.oracle.com/pls/topic/lookup?ctx=microtx-latest&id=TMMLI-GUID-97C20FE6-4DA2-4699-96EB-BB26472FCCBE\n- https://docs.oracle.com/pls/topic/lookup?ctx=microtx-latest&id=TMMDG-GUID-F6ED47D2-97FE-481E-A41E-C320A3611C0B\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2745,"content_sha256":"c510850a37c7dd057a78e35ad4ed7bdc20b8f0c0a26190a6915f01a981a40b5e"},{"filename":"containers/observability-exporter.md","content":"# `observability-exporter` OCR Repository\n\n## Overview\n\n`database/observability-exporter` is the Oracle Container Registry repository for Oracle Database Observability Exporter (Metrics, Logs, and Tracing). Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/observability-exporter`\n- **OCR short description:** Oracle Database Observability Exporter (Metrics, Logs, and Tracing)\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/observability-exporter:2.2.2`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Universal Permissive License (UPL).\n\n## What Oracle Documents Here\n\n- The OCR detail page describes this image as the Unified Observability Exporter for Oracle Database.\n- The page says it contains OpenTelemetry exporters for metrics, logs, and tracing.\n- OCR also notes that the exporters are configurable for targets such as Prometheus, Promtail/Loki, Jaeger, and Grafana-based observability workflows.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page presents this repository as a tool image with versioned exporter tags, not as a 19c-versus-26ai database image line. Use the OCR tags table to select the exporter release you need.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need DB metrics/logs/traces export into observability stacks.\n- **Use another image when:** Avoid when you need a database server image; this is tooling/exporter only.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/observability-exporter:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/observability-exporter:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/observability-exporter\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2366,"content_sha256":"982a374d1573da6deaaf9e2362bb876ddb8b3a440282abd0a46caf0d03f8bd7e"},{"filename":"containers/operator.md","content":"# `operator` OCR Repository\n\n## Overview\n\n`database/operator` is the Oracle Container Registry repository for This image is part of and for use with the Oracle Database Operator for Kubernetes. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/operator`\n- **OCR short description:** This image is part of and for use with the Oracle Database Operator for Kubernetes\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/operator:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Universal Permissive License (UPL).\n\n## What Oracle Documents Here\n\n- The OCR detail page says this image is part of and for use with the Oracle Database Operator for Kubernetes.\n- The page describes the operator as an open source system that extends the Kubernetes API with custom resources and controllers for Oracle Database lifecycle automation.\n- OCR points to the operator readme for installation and usage details, and the tags table includes both `latest` and versioned operator releases.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page tracks operator-image releases instead of a 19c-versus-26ai database matrix. Use the repository tags table and the operator readme to match the operator version to your Kubernetes environment.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when Oracle Database lifecycle is managed through Kubernetes operator patterns.\n- **Use another image when:** Avoid when you are not using Kubernetes operator-based operations.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Kubernetes cluster and operator installation prerequisites apply; follow operator docs from OCR.\n- **Pull:** `docker pull container-registry.oracle.com/database/operator:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/operator:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/operator\n- https://github.com/oracle/oracle-database-operator#readme\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2469,"content_sha256":"64d1d992eec7fddc648cf7ab1015b32010435d6e02445c37bdb0d8a35a7d2a3c"},{"filename":"containers/ords.md","content":"# `ords` OCR Repository\n\n## Overview\n\n`database/ords` is the Oracle Container Registry repository for Oracle REST Data Services (ORDS) command line interface.. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/ords`\n- **OCR short description:** Oracle REST Data Services (ORDS) command line interface.\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/ords:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Oracle Free Use Terms and Conditions provided in the container image.\n\n## What Oracle Documents Here\n\n- The OCR detail page presents this repository as the Oracle REST Data Services command line interface container image.\n- The page links to Oracle documentation for installing and configuring customer-managed ORDS against Autonomous Database.\n- OCR publishes both a latest pull command and a tags table so you can choose the ORDS image version you want to run.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page tracks ORDS image releases instead of a 19c-versus-26ai database matrix. Use the OCR tags table and the linked ORDS documentation to select the image version that matches your deployment target.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use for supported ORDS container deployments.\n- **Use another image when:** Avoid deprecated/legacy ORDS container lines for new deployments.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/ords:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/ords:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/ords\n- https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/25.2/ordig/installing-and-configuring-customer-managed-ords-autonomous-database.html#GUID-AC7F9A42-A7C2-4453-B8D1-BFD2784C3CA0\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2450,"content_sha256":"bf8933424e307eae14971615e2b2de1d0d4dcb1698c5cb9f39fa26df78f3c1e2"},{"filename":"containers/otmm.md","content":"# `otmm` OCR Repository\n\n## Overview\n\n`database/otmm` is the Oracle Container Registry repository for Oracle Transaction Manager for Microservices (MicroTx) Free. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/otmm`\n- **OCR short description:** Oracle Transaction Manager for Microservices (MicroTx) Free\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/otmm:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Oracle Free Use Terms and Conditions provided in the container image.\n\n## What Oracle Documents Here\n\n- The OCR readme describes `otmm` as the free MicroTx image for helping maintain consistency across distributed microservices applications.\n- The page says MicroTx supports XA, Saga based on Eclipse MicroProfile Long Running Action (LRA), and Try-Confirm/Cancel (TCC) style workflows.\n- OCR links the install-and-use workflow and publishes both `latest` and versioned MicroTx tags for this repository.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page tracks MicroTx image releases rather than a 19c-versus-26ai database matrix. Use the repository tags table to choose the MicroTx release you need.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need free MicroTx capabilities for distributed transaction patterns.\n- **Use another image when:** Avoid when you need Enterprise coordinator/console features; use microtx-ee-* images.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/otmm:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/otmm:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/otmm\n- https://docs.oracle.com/pls/topic/lookup?ctx=microtx-latest&id=TMMDG-GUID-F6ED47D2-97FE-481E-A41E-C320A3611C0B\n- https://www.oracle.com/downloads/licenses/oracle-free-license.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2489,"content_sha256":"29cffc03d3bf0cbc58391ba1c118471b74e19f4373d4a8d3d8b93944a388188d"},{"filename":"containers/private-ai.md","content":"# `private-ai` OCR Repository\n\n## Overview\n\n`database/private-ai` is the Oracle Container Registry repository for Oracle Private AI Services Container. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/private-ai`\n- **OCR short description:** Oracle Private AI Services Container\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/private-ai:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme documents both non-secure HTTP mode and secure HTTPS-plus-authentication mode for the Private AI Services container.\n- The page says that, as of version `25.1.2.0.0`, the image includes scripts that simplify secure and non-secure setup and can be copied out of the image for guided configuration.\n- OCR also says that `25.x.x.x.x` versions of the service support ONNX embedding pipeline models only, and points to OML4Py guidance for model conversion.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page explicitly calls out 25.x image behavior. In particular, it notes setup scripts starting with `25.1.2.0.0` and states that the 25.x service supports ONNX embedding pipeline models only.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when deploying Oracle Private AI Services container endpoints.\n- **Use another image when:** Avoid when you only need a base database runtime without AI service APIs.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/private-ai:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/private-ai:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/private-ai\n- https://docs.oracle.com/en/database/oracle/oracle-database/26/prvai/index.html\n- https://docs.oracle.com/en/database/oracle/machine-learning/oml4py/2-23ai/mlpug/convert-pretrained-models-onnx-model-end-end-instructions.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2654,"content_sha256":"5aad180063f8697b215c572c7b1c2f6f169d73641ffeea3116379a2540096770"},{"filename":"containers/rac_ru.md","content":"# `rac_ru` OCR Repository\n\n## Overview\n\n`database/rac_ru` is the Oracle Container Registry repository for Oracle Real Application Cluster Release Update Container Images. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/rac_ru`\n- **OCR short description:** Oracle Real Application Cluster Release Update Container Images\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/rac_ru:latest-19`\n- **License note on OCR:** OCR states that you must accept the Oracle Container Registry Critical Patch Update (CPU) Repository Terms and Restrictions before downloading from this repository.\n\n## What Oracle Documents Here\n\n- The OCR readme describes `rac_ru` as the Release Update container-image line for Oracle RAC in Linux containers.\n- The page says RAC on Podman is supported starting with 19c (19.16) and 21c (21.7) for this Release Update stream.\n- OCR links the RAC installation guide for Podman on Oracle Linux and covers preparation, network planning, storage, and password management.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page explicitly positions `rac_ru` as a Release Update image stream and documents support on Podman starting with 19c (19.16) and 21c (21.7). The latest OCR pull command currently uses `latest-19`.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need RU-tagged Oracle RAC container images under CPU terms.\n- **Use another image when:** Avoid when you need the non-RU stream; use rac.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Use Podman-based prerequisites from OCR RAC RU docs, including network/storage planning.\n- **Pull:** `docker pull container-registry.oracle.com/database/rac_ru:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/rac_ru:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/rac_ru\n- https://docs.oracle.com/cd/F39414_01/racpd/oracle-real-application-clusters-installation-guide-podman-oracle-linux-x86-64.pdf\n- https://docs.oracle.com/en/database/oracle/oracle-database/21/racpd/target-configuration-oracle-rac-podman.html#GUID-59138DF8-3781-4033-A38F-E0466884D008\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2609,"content_sha256":"570d7c7f28426f67d917001ac53d577361db6b4b551f747b4ec8909f43c8677f"},{"filename":"containers/rac.md","content":"# `rac` OCR Repository\n\n## Overview\n\n`database/rac` is the Oracle Container Registry repository for Oracle Real Application Clusters. Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/rac`\n- **OCR short description:** Oracle Real Application Clusters\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/rac:latest`\n- **License note on OCR:** OCR presents this as a standard Oracle repository. The detail page prompts you to sign in with an Oracle account to accept the repository license agreement before downloading the image.\n\n## What Oracle Documents Here\n\n- The OCR readme describes `rac` as Oracle Real Application Clusters in Linux containers and covers preparation, installation, and validation steps.\n- The page says RAC containers are supported for production use on Podman starting with Oracle Database 19c (19.16), 21c (21.7), and 23.26ai (26ai).\n- OCR links the RAC installation guide for Podman on Oracle Linux and calls out preparation topics such as SELinux labeling, storage, and network planning.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR readme explicitly calls out support on Podman starting with 19c (19.16), 21c (21.7), and 23.26ai (26ai). Use the repository tags table to select the exact RAC container version.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use when you need Oracle RAC container deployments with Podman guidance.\n- **Use another image when:** Avoid when you need RU stream tags; use rac_ru.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Use Podman-based prerequisites from OCR RAC docs, including network/storage planning.\n- **Pull:** `docker pull container-registry.oracle.com/database/rac:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/rac:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/rac\n- https://docs.oracle.com/cd/F39414_01/racpd/oracle-real-application-clusters-installation-guide-podman-oracle-linux-x86-64.pdf\n- https://docs.oracle.com/en/database/oracle/oracle-database/21/racpd/target-configuration-oracle-rac-podman.html#GUID-59138DF8-3781-4033-A38F-E0466884D008\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2595,"content_sha256":"5e03dc1a84921606acefea0a67bb30f2cdaba624a57a1527592f821cdfc5a528"},{"filename":"containers/SKILLS.md","content":"# Container Skills Index\n\nComplete index for container-related skills in `db/containers/`.\n\n## Common\n\n| File | Description |\n|------|-------------|\n| `adb-free.md` | Oracle Autonomous Database Free container image with ADW/ATP guidance |\n| `enterprise.md` | Oracle AI Database Server Release 26ai Enterprise Edition container image |\n| `enterprise_ru.md` | Oracle Database Enterprise Edition CPU release-update image repository |\n| `free.md` | Oracle AI Database 26ai Free container image |\n| `instantclient.md` | Oracle Instant Client container image with Basic, SDK, and SQL*Plus packages |\n| `ords.md` | Oracle REST Data Services container image repository |\n| `rac.md` | Oracle RAC container deployment guidance for Podman |\n| `rac_ru.md` | Oracle RAC release-update container image repository |\n| `sqlcl.md` | Oracle SQL Command Line (SQLcl) container image repository |\n| `container-selection-matrix.md` | Decision matrix for choosing the right OCR database-category container image |\n\n## Advanced / Niche\n\n| File | Description |\n|------|-------------|\n| `cman.md` | Oracle Connection Manager container repository, proxying client connections and OCR pull guidance |\n| `graph-quickstart.md` | Property Graph quickstart image built on Oracle AI Database 26ai Free |\n| `gsm.md` | Oracle Global Service Manager container for Globally Distributed Database deployments |\n| `gsm_ru.md` | Oracle Global Service Manager CPU repository stream |\n| `microtx-ee-console.md` | Oracle Transaction Manager for Microservices Console container image |\n| `microtx-ee-coordinator.md` | Oracle Transaction Manager for Microservices Enterprise Edition coordinator image |\n| `observability-exporter.md` | Unified observability exporter image for Oracle Database metrics, logs, and tracing |\n| `operator.md` | Oracle Database Operator for Kubernetes image repository |\n| `otmm.md` | Oracle Transaction Manager for Microservices Free image repository |\n| `private-ai.md` | Oracle Private AI Services container image, setup modes, and OCR version notes |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2037,"content_sha256":"05730cf5b6b66ac4a8cebd1c563bc0c89deb2f3f32ea171f2b5a7e2c849fba63"},{"filename":"containers/sqlcl.md","content":"# `sqlcl` OCR Repository\n\n## Overview\n\n`database/sqlcl` is the Oracle Container Registry repository for Oracle SQL Command Line (SQLcl). Oracle lists this repository in the OCR Database business area and publishes both a latest pull command and a tags table for selecting concrete image versions.\n\n## Repository Snapshot\n\n- **Registry path:** `container-registry.oracle.com/database/sqlcl`\n- **OCR short description:** Oracle SQL Command Line (SQLcl)\n- **Latest pull command shown on OCR:** `docker pull container-registry.oracle.com/database/sqlcl:latest`\n- **License note on OCR:** OCR states that the software in this repository is licensed under the Oracle Free Use Terms and Conditions provided in the container image.\n\n## What Oracle Documents Here\n\n- The OCR readme documents this repository as the Oracle SQLcl Docker image and describes SQLcl as a free command line interface for Oracle Database.\n- The page says the image contains the latest SQLcl release available and can be used anywhere Docker can run.\n- OCR also documents running SQLcl interactively, passing standard SQLcl options on the `docker run` command line, and mounting `/opt/oracle/sql_scripts` for local script access.\n\n## Oracle Version Notes (19c vs 26ai)\n\nThe OCR detail page tracks SQLcl image releases instead of a 19c-versus-26ai database matrix. The page itself is titled as SQLcl 25.4.2 Docker image documentation, so use the repository tags table to pin the release you want.\n\n## When to Use / When Not to Use\n\n- **Use this image when:** Use for SQLcl-based scripting and CI automation in a containerized CLI.\n- **Use another image when:** Avoid when you need ORDS runtime or database server capabilities.\n- **Cross-image decision aid:** `db/containers/container-selection-matrix.md`\n\n## Prerequisites and Minimal Run Pattern\n\n- **Prerequisite:** Accept OCR repository terms and authenticate to container-registry.oracle.com before pull.\n- **Pull:** `docker pull container-registry.oracle.com/database/sqlcl:\u003ctag>`\n- **Run pattern:** `docker run --name \u003cname> --rm -it container-registry.oracle.com/database/sqlcl:\u003ctag>`\n- **Important:** Use the OCR README example command for exact environment variables, mounted volumes, and published ports for this image.\n\n## Sources\n\n- https://container-registry.oracle.com/ords/ocr/ba/database/sqlcl\n- https://www.oracle.com/database/technologies/appdev/sqlcl.html\n- https://www.oracle.com/downloads/licenses/oracle-free-license.html\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2459,"content_sha256":"0574840ab6eb76097855fbf29b9785069032b660e4e840ab9e834e56732213a4"},{"filename":"design/data-modeling.md","content":"# Data Modeling for Oracle Database\n\n## Overview\n\nData modeling is the process of defining how data is organized, stored, and related within a database. It operates across three abstraction levels: **conceptual** (business concepts), **logical** (platform-independent relational structure), and **physical** (Oracle-specific DDL with storage parameters). This guide covers logical and physical modeling techniques, data warehouse schema patterns, Operational Data Store design, and Oracle-specific physical model considerations including storage clauses, compression, and partitioning.\n\n---\n\n## 1. Modeling Levels\n\n### Conceptual Model\n\nThe conceptual model captures business entities and their high-level relationships without any technical detail. It is tool-agnostic and intended for communication with business stakeholders.\n\n- Entities: Customer, Order, Product\n- Relationships: Customer places Order, Order contains Product\n- No data types, no keys, no constraints\n\n### Logical Model\n\nThe logical model is platform-independent but technically complete. It defines:\n\n- All entities as tables with column names and data types (generic: STRING, INTEGER, DECIMAL)\n- Primary keys, foreign keys, and candidate keys\n- Normalization applied (typically to 3NF for OLTP, denormalized for OLAP)\n- Constraints and business rules (referential integrity, NOT NULL)\n\n### Physical Model\n\nThe physical model is Oracle-specific DDL. It adds:\n\n- Oracle data types (`VARCHAR2`, `NUMBER`, `DATE`, `TIMESTAMP`, `CLOB`, `BLOB`)\n- Storage clauses (`TABLESPACE`, `STORAGE`, `PCTFREE`, `PCTUSED`)\n- Partitioning strategy\n- Index definitions\n- Compression settings\n- Parallel query hints\n\n---\n\n## 2. OLTP Logical Modeling\n\nOnline Transaction Processing (OLTP) systems prioritize **write performance**, **data integrity**, and **concurrency**. The logical model for OLTP follows normalization rules closely (3NF minimum).\n\n### Key Characteristics\n\n- High insert/update/delete volume\n- Small, targeted queries (single-row or narrow range lookups)\n- Many short transactions with high concurrency\n- Row-level locking is critical\n- Normalized to minimize redundancy and lock contention\n\n### Sample OLTP Schema\n\n```sql\n-- Normalized OLTP schema for an e-commerce system\n\nCREATE TABLE CUSTOMERS (\n customer_id NUMBER GENERATED ALWAYS AS IDENTITY,\n email VARCHAR2(255) NOT NULL,\n first_name VARCHAR2(100) NOT NULL,\n last_name VARCHAR2(100) NOT NULL,\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n CONSTRAINT pk_customers PRIMARY KEY (customer_id),\n CONSTRAINT uq_customers_email UNIQUE (email)\n)\nTABLESPACE users_data\nPCTFREE 10;\n\nCREATE TABLE ORDERS (\n order_id NUMBER GENERATED ALWAYS AS IDENTITY,\n customer_id NUMBER NOT NULL,\n order_date TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n status VARCHAR2(20) DEFAULT 'PENDING' NOT NULL,\n total_amount NUMBER(14,2),\n CONSTRAINT pk_orders PRIMARY KEY (order_id),\n CONSTRAINT fk_orders_cust FOREIGN KEY (customer_id) REFERENCES CUSTOMERS (customer_id),\n CONSTRAINT ck_order_status CHECK (status IN ('PENDING','CONFIRMED','SHIPPED','DELIVERED','CANCELLED'))\n)\nTABLESPACE users_data\nPCTFREE 15; -- higher PCTFREE for rows likely to grow via updates\n\nCREATE INDEX IX_ORDERS_CUSTOMER_ID ON ORDERS (customer_id) TABLESPACE users_idx;\nCREATE INDEX IX_ORDERS_ORDER_DATE ON ORDERS (order_date) TABLESPACE users_idx;\n```\n\n---\n\n## 3. Data Warehouse Dimensional Modeling\n\nData warehouses prioritize **read performance** for analytical queries over large data volumes. **Dimensional modeling** (Ralph Kimball methodology) structures data into **fact tables** and **dimension tables**.\n\n### Fact Tables\n\nFact tables store measurable business events (sales, transactions, events). They contain:\n\n- Foreign keys to dimension tables\n- Numeric measures (quantity, amount, duration)\n- Degenerate dimensions (order number stored in the fact, not a separate dimension)\n- Typically very large (hundreds of millions to billions of rows)\n\n### Dimension Tables\n\nDimension tables store descriptive context for facts. They contain:\n\n- A surrogate primary key (not the natural/business key)\n- Descriptive attributes (names, categories, hierarchies)\n- Typically much smaller than fact tables\n- Updated infrequently (Slowly Changing Dimensions)\n\n---\n\n## 4. Star Schema\n\nThe **star schema** is the simplest and most query-efficient dimensional model. The fact table is in the center; dimension tables radiate outward like a star. There is no normalization between dimension tables — all descriptive attributes are collapsed into a single wide dimension table.\n\n```\n DIM_DATE\n |\nDIM_CUSTOMER -- FACT_SALES -- DIM_PRODUCT\n |\n DIM_STORE\n```\n\n### Star Schema DDL Example\n\n```sql\n-- Dimension: Date\nCREATE TABLE DIM_DATE (\n date_key NUMBER(8) NOT NULL, -- YYYYMMDD surrogate key\n full_date DATE NOT NULL,\n day_of_week VARCHAR2(10) NOT NULL,\n day_of_month NUMBER(2) NOT NULL,\n month_number NUMBER(2) NOT NULL,\n month_name VARCHAR2(10) NOT NULL,\n quarter_number NUMBER(1) NOT NULL,\n year_number NUMBER(4) NOT NULL,\n is_weekend CHAR(1) DEFAULT 'N' NOT NULL,\n is_holiday CHAR(1) DEFAULT 'N' NOT NULL,\n CONSTRAINT pk_dim_date PRIMARY KEY (date_key)\n)\nTABLESPACE dw_data\nCOMPRESS FOR QUERY HIGH; -- Hybrid Columnar Compression (HCC) — requires Exadata or Oracle ZFS/ODA storage\n\n-- Dimension: Customer (denormalized — city/state/country collapsed in)\nCREATE TABLE DIM_CUSTOMER (\n customer_key NUMBER GENERATED ALWAYS AS IDENTITY,\n customer_bk VARCHAR2(50) NOT NULL, -- business/natural key\n full_name VARCHAR2(200) NOT NULL,\n email VARCHAR2(255),\n city VARCHAR2(100),\n state_province VARCHAR2(100),\n country_code CHAR(2),\n customer_segment VARCHAR2(50),\n effective_from DATE NOT NULL,\n effective_to DATE,\n is_current CHAR(1) DEFAULT 'Y' NOT NULL,\n CONSTRAINT pk_dim_customer PRIMARY KEY (customer_key)\n)\nTABLESPACE dw_data\nCOMPRESS FOR QUERY HIGH;\n\n-- Dimension: Product (denormalized — category hierarchy collapsed in)\nCREATE TABLE DIM_PRODUCT (\n product_key NUMBER GENERATED ALWAYS AS IDENTITY,\n product_bk VARCHAR2(50) NOT NULL,\n product_name VARCHAR2(200) NOT NULL,\n product_desc VARCHAR2(1000),\n subcategory_name VARCHAR2(100),\n category_name VARCHAR2(100),\n brand_name VARCHAR2(100),\n unit_cost NUMBER(12,2),\n unit_price NUMBER(12,2),\n is_active CHAR(1) DEFAULT 'Y' NOT NULL,\n CONSTRAINT pk_dim_product PRIMARY KEY (product_key)\n)\nTABLESPACE dw_data\nCOMPRESS FOR QUERY HIGH;\n\n-- Fact: Sales (central fact table)\nCREATE TABLE FACT_SALES (\n sales_id NUMBER GENERATED ALWAYS AS IDENTITY,\n date_key NUMBER(8) NOT NULL,\n customer_key NUMBER NOT NULL,\n product_key NUMBER NOT NULL,\n store_key NUMBER NOT NULL,\n order_number VARCHAR2(50), -- degenerate dimension\n quantity_sold NUMBER(10) NOT NULL,\n unit_price NUMBER(12,2) NOT NULL,\n unit_cost NUMBER(12,2) NOT NULL,\n gross_revenue NUMBER(14,2) NOT NULL,\n gross_profit NUMBER(14,2) NOT NULL,\n discount_amount NUMBER(12,2) DEFAULT 0 NOT NULL,\n CONSTRAINT pk_fact_sales PRIMARY KEY (sales_id),\n CONSTRAINT fk_fs_date FOREIGN KEY (date_key) REFERENCES DIM_DATE (date_key),\n CONSTRAINT fk_fs_customer FOREIGN KEY (customer_key) REFERENCES DIM_CUSTOMER (customer_key),\n CONSTRAINT fk_fs_product FOREIGN KEY (product_key) REFERENCES DIM_PRODUCT (product_key),\n CONSTRAINT fk_fs_store FOREIGN KEY (store_key) REFERENCES DIM_STORE (store_key)\n)\nTABLESPACE dw_data\nCOMPRESS FOR QUERY HIGH\nPARTITION BY RANGE (date_key) (\n PARTITION p_2023 VALUES LESS THAN (20240101),\n PARTITION p_2024 VALUES LESS THAN (20250101),\n PARTITION p_2025 VALUES LESS THAN (20260101),\n PARTITION p_future VALUES LESS THAN (MAXVALUE)\n);\n\n-- Bitmap indexes — extremely efficient for low-cardinality FK columns in DW\nCREATE BITMAP INDEX BIX_FS_DATE ON FACT_SALES (date_key) LOCAL TABLESPACE dw_idx;\nCREATE BITMAP INDEX BIX_FS_CUSTOMER ON FACT_SALES (customer_key) LOCAL TABLESPACE dw_idx;\nCREATE BITMAP INDEX BIX_FS_PRODUCT ON FACT_SALES (product_key) LOCAL TABLESPACE dw_idx;\n```\n\n**Note:** Bitmap indexes are ideal for data warehouse FK columns (low cardinality, read-heavy, infrequent DML). Never use bitmap indexes on OLTP tables with high concurrent writes — they cause severe lock contention.\n\n> **Important:** `COMPRESS FOR QUERY HIGH` uses Oracle Hybrid Columnar Compression (HCC). HCC is **not** a general Advanced Compression feature — it requires Exadata, Oracle ZFS Storage Appliance, Oracle Database Appliance (ODA), or another HCC-compatible engineered system. On standard server storage, this clause will not achieve columnar compression. For non-Exadata environments use `ROW STORE COMPRESS ADVANCED` (Advanced Row Compression, requires Advanced Compression option) or `COMPRESS BASIC` (direct-path inserts only, all editions).\n\n---\n\n## 5. Snowflake Schema\n\nThe **snowflake schema** normalizes dimension tables, splitting out sub-hierarchies into separate tables. This reduces storage for dimension data but introduces additional joins.\n\n```\nDIM_PRODUCT_CATEGORY\n |\n DIM_PRODUCT -- FACT_SALES -- DIM_CUSTOMER -- DIM_GEOGRAPHY\n |\n DIM_DATE\n```\n\n### Snowflake DDL Example\n\n```sql\n-- Normalized product dimension hierarchy\nCREATE TABLE DIM_PRODUCT_CATEGORY (\n category_key NUMBER GENERATED ALWAYS AS IDENTITY,\n category_name VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_dim_prod_cat PRIMARY KEY (category_key)\n);\n\nCREATE TABLE DIM_PRODUCT_SUBCATEGORY (\n subcategory_key NUMBER GENERATED ALWAYS AS IDENTITY,\n subcategory_name VARCHAR2(100) NOT NULL,\n category_key NUMBER NOT NULL,\n CONSTRAINT pk_dim_prod_subcat PRIMARY KEY (subcategory_key),\n CONSTRAINT fk_subcat_category FOREIGN KEY (category_key)\n REFERENCES DIM_PRODUCT_CATEGORY (category_key)\n);\n\nCREATE TABLE DIM_PRODUCT (\n product_key NUMBER GENERATED ALWAYS AS IDENTITY,\n product_bk VARCHAR2(50) NOT NULL,\n product_name VARCHAR2(200) NOT NULL,\n subcategory_key NUMBER NOT NULL,\n unit_price NUMBER(12,2),\n CONSTRAINT pk_dim_product PRIMARY KEY (product_key),\n CONSTRAINT fk_prod_subcategory FOREIGN KEY (subcategory_key)\n REFERENCES DIM_PRODUCT_SUBCATEGORY (subcategory_key)\n);\n```\n\n### Star vs Snowflake: When to Use Each\n\n| Criteria | Star Schema | Snowflake Schema |\n|---|---|---|\n| Query performance | Better (fewer joins) | Slower (more joins) |\n| Storage | More (denormalized dimensions) | Less (normalized dimensions) |\n| ETL complexity | Simpler | More complex |\n| Maintenance | Harder (update in many rows) | Easier (update dimension table) |\n| BI tool compatibility | Better (most tools prefer star) | Acceptable |\n| Best for | Most DW use cases | Very large dimensions with deep hierarchies |\n\n---\n\n## 6. Operational Data Store (ODS)\n\nAn **Operational Data Store** bridges OLTP source systems and the data warehouse. It provides:\n\n- Near-real-time integrated operational reporting\n- A staging/cleansing layer before DW loading\n- A single integrated view across multiple source systems\n\n### ODS Design Principles\n\n- **Subject-oriented**: Organized around business subjects, not source systems\n- **Integrated**: Data from multiple sources reconciled into a common model\n- **Current**: Reflects near-real-time operational state (unlike DW which is historical)\n- **Volatile**: ODS data is updated in-place (unlike DW which is append-only)\n\n```sql\n-- ODS table with source system tracking and audit columns\nCREATE TABLE ODS_CUSTOMER (\n ods_customer_id NUMBER GENERATED ALWAYS AS IDENTITY,\n -- Source system tracking\n source_system VARCHAR2(50) NOT NULL, -- 'CRM', 'ERP', 'WEB'\n source_system_id VARCHAR2(100) NOT NULL,\n -- Business key (unified across source systems)\n customer_email VARCHAR2(255) NOT NULL,\n -- Integrated attributes\n full_name VARCHAR2(200),\n phone_number VARCHAR2(30),\n address_line1 VARCHAR2(255),\n city VARCHAR2(100),\n country_code CHAR(2),\n customer_status VARCHAR2(20),\n -- ODS audit columns\n source_created_at TIMESTAMP,\n source_updated_at TIMESTAMP,\n ods_loaded_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n ods_updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n ods_checksum VARCHAR2(64), -- MD5/SHA of key fields for change detection\n CONSTRAINT pk_ods_customer PRIMARY KEY (ods_customer_id),\n CONSTRAINT uq_ods_cust_src UNIQUE (source_system, source_system_id)\n)\nTABLESPACE ods_data;\n\n-- Index for common ODS lookup patterns\nCREATE INDEX IX_ODS_CUST_EMAIL ON ODS_CUSTOMER (customer_email) TABLESPACE ods_idx;\nCREATE INDEX IX_ODS_CUST_LOADED ON ODS_CUSTOMER (ods_loaded_at) TABLESPACE ods_idx;\n```\n\n---\n\n## 7. Slowly Changing Dimensions (SCD)\n\nSCDs manage changes to dimension data over time. Oracle supports all three common types.\n\n### SCD Type 1 — Overwrite\n\nNo history retained. Simplest approach. Used when history is not relevant.\n\n```sql\nUPDATE DIM_CUSTOMER\nSET email = :new_email,\n ods_updated_at = SYSTIMESTAMP\nWHERE customer_bk = :customer_bk\nAND is_current = 'Y';\n```\n\n### SCD Type 2 — Add New Row (Full History)\n\nA new row is inserted for every change. Previous row is closed out.\n\n```sql\n-- Close the current record\nUPDATE DIM_CUSTOMER\nSET effective_to = TRUNC(SYSDATE) - INTERVAL '1' SECOND,\n is_current = 'N'\nWHERE customer_bk = :customer_bk\nAND is_current = 'Y';\n\n-- Insert the new current record\nINSERT INTO DIM_CUSTOMER (\n customer_bk, full_name, email, city, state_province, country_code,\n customer_segment, effective_from, effective_to, is_current\n) VALUES (\n :customer_bk, :full_name, :email, :city, :state_province, :country_code,\n :customer_segment, TRUNC(SYSDATE), NULL, 'Y'\n);\n```\n\n### SCD Type 3 — Previous Value Column\n\nStores the current and one previous value. Limited history but simple queries.\n\n```sql\nALTER TABLE DIM_CUSTOMER ADD (\n previous_email VARCHAR2(255),\n email_changed_date DATE\n);\n\nUPDATE DIM_CUSTOMER\nSET previous_email = email,\n email_changed_date = SYSDATE,\n email = :new_email\nWHERE customer_bk = :customer_bk;\n```\n\n---\n\n## 8. Oracle Physical Model Considerations\n\n### Oracle Data Types\n\nChoose data types carefully for storage efficiency and correctness:\n\n```sql\nCREATE TABLE PHYSICAL_MODEL_EXAMPLE (\n -- Numeric types\n id NUMBER(10) NOT NULL, -- integers up to 10 digits\n amount NUMBER(18,4) NOT NULL, -- financial: precision + scale\n percentage NUMBER(5,2) NOT NULL, -- 999.99\n\n -- Character types\n short_code CHAR(3) NOT NULL, -- fixed-length: ISO codes\n description VARCHAR2(4000) NOT NULL, -- variable, up to 4000 bytes\n large_text CLOB, -- > 4000 chars\n json_data CLOB CHECK (json_data IS JSON),-- JSON validation (12c+)\n\n -- Date/Time types\n event_date DATE NOT NULL, -- date + time (no TZ)\n created_at TIMESTAMP(6) NOT NULL, -- microsecond precision\n updated_at TIMESTAMP WITH TIME ZONE, -- global apps: always store TZ\n duration_days INTERVAL DAY(3) TO SECOND(0), -- elapsed time\n\n -- Binary types\n file_content BLOB, -- binary files\n thumbnail RAW(2000), -- small binary (\u003c= 2000 bytes)\n\n CONSTRAINT pk_pme PRIMARY KEY (id)\n);\n```\n\n### Storage Clauses\n\nOracle storage parameters control physical space allocation within a segment:\n\n```sql\nCREATE TABLE ORDERS (\n order_id NUMBER NOT NULL,\n order_data VARCHAR2(500),\n CONSTRAINT pk_orders PRIMARY KEY (order_id)\n)\nTABLESPACE users_data\nPCTFREE 15 -- 15% of each block reserved for row updates (UPDATE growth)\nPCTUSED 40 -- block re-eligible for inserts when used% drops below 40 (MSSM only)\nINITRANS 4 -- initial transaction slots per block (higher for concurrent DML)\nMAXTRANS 255 -- maximum concurrent transactions per block\nSTORAGE (\n INITIAL 64K -- initial extent size\n NEXT 64K -- subsequent extent sizes (locally managed: ignored)\n MINEXTENTS 1 -- minimum number of extents\n MAXEXTENTS UNLIMITED\n PCTINCREASE 0 -- no geometric growth (locally managed: ignored)\n);\n```\n\n**PCTFREE tuning guide:**\n\n| Workload | Recommended PCTFREE | Rationale |\n|---|---|---|\n| Insert-only (append) | 0–5 | Rows never updated; maximize block density |\n| Mix of inserts + updates | 10–20 | Reserve space for row growth |\n| Heavy updates (row growth) | 25–40 | Prevent row chaining/migration |\n| Data warehouse (query only) | 0–5 | Maximize scan density |\n\n### Oracle Compression\n\nOracle provides multiple compression tiers:\n\n```sql\n-- Basic Compression (all editions) — compresses during direct-path inserts only\nCREATE TABLE SALES_ARCHIVE (\n sale_id NUMBER,\n sale_date DATE,\n amount NUMBER(14,2)\n)\nCOMPRESS BASIC\nTABLESPACE dw_data;\n\n-- Advanced Row Compression (Enterprise) — compresses all DML operations\nCREATE TABLE ORDERS (\n order_id NUMBER,\n customer_id NUMBER,\n order_date DATE\n)\nROW STORE COMPRESS ADVANCED\nTABLESPACE users_data;\n\n-- Hybrid Columnar Compression — REQUIRES Exadata, ZFS Storage Appliance, or ODA\n-- Not available on standard server/SAN storage; will silently fall back to no compression\nCREATE TABLE FACT_SALES (\n date_key NUMBER(8),\n amount NUMBER(14,2)\n)\nCOMPRESS FOR QUERY HIGH\nTABLESPACE dw_data;\n\n-- In-Memory Compression (12c 12.1.0.2+) — in-memory columnar store\n-- MEMCOMPRESS and PRIORITY must be combined in a single INMEMORY clause\nALTER TABLE FACT_SALES\n INMEMORY MEMCOMPRESS FOR QUERY HIGH PRIORITY CRITICAL;\n```\n\n### Parallel Query Configuration\n\nFor data warehouse tables, configure parallel DML and query:\n\n```sql\n-- Enable parallel query on a table (DW fact table example)\nALTER TABLE FACT_SALES PARALLEL 8;\n\n-- Session-level parallel DML\nALTER SESSION ENABLE PARALLEL DML;\n\n-- Hint-based parallel query\nSELECT /*+ PARALLEL(f, 8) PARALLEL(d, 4) */\n d.year_number,\n SUM(f.gross_revenue) AS total_revenue\nFROM FACT_SALES f\nJOIN DIM_DATE d ON f.date_key = d.date_key\nGROUP BY d.year_number\nORDER BY d.year_number;\n```\n\n---\n\n## 9. Best Practices\n\n- **Separate OLTP and DW schemas** into different tablespaces (and ideally different databases or PDBs) to isolate I/O profiles and backup strategies.\n- **Use surrogate keys in dimension tables**, never business/natural keys as dimension primary keys. Natural keys change; surrogate keys never do.\n- **Pre-aggregate judiciously.** Oracle materialized views can serve as pre-aggregated summaries that the query optimizer uses automatically (query rewrite).\n- **Apply compression to data warehouse tables.** On Exadata (HCC), `COMPRESS FOR QUERY HIGH` achieves 10x or more compression on fact tables. On non-Exadata environments, use `ROW STORE COMPRESS ADVANCED` (Advanced Compression option required) which typically achieves 2:1 to 4:1 ratios for DW bulk loads.\n- **Design for ETL patterns.** Include audit columns (`LOAD_DATE`, `SOURCE_SYSTEM`, `BATCH_ID`) in every DW table from day one — retrofitting them is expensive.\n- **Avoid triggers on DW fact tables.** High-volume bulk loads with row-level triggers destroy performance. Use ETL logic in the load process instead.\n- **Document the grain** of every fact table explicitly — the grain is the most precise level of detail the fact table records (e.g., \"one row per order line item per day\").\n\n---\n\n## 10. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Using OLTP Schema for Analytical Queries\n\nRunning analytical queries against a normalized OLTP schema produces massive join trees and full table scans. Maintain a separate dimensional model (star schema) for analytics.\n\n### Mistake 2: Using Operational Primary Keys as Dimension Surrogate Keys\n\nBusiness keys (order numbers, product SKUs, customer emails) change over time. Always generate a new surrogate key for dimension tables.\n\n### Mistake 3: Missing SCD Strategy\n\nFailing to decide on SCD type before go-live means historical changes are silently lost. Document and implement SCD type per dimension table before the first data load.\n\n### Mistake 4: Not Partitioning Large Fact Tables\n\nFact tables exceeding 50–100 million rows without partitioning will suffer full table scans and extremely slow maintenance operations (archiving, deletes, statistics gathering).\n\n```sql\n-- Add range partitioning to an existing large table (Oracle 12.2+ online)\nALTER TABLE FACT_SALES MODIFY\n PARTITION BY RANGE (date_key) INTERVAL (10000) (\n PARTITION p_initial VALUES LESS THAN (20200101)\n )\n ONLINE;\n```\n\n### Mistake 5: Bitmap Indexes on OLTP Tables\n\nBitmap indexes lock at the bitmap segment level during DML — a single insert or update to a fact table can block dozens of concurrent transactions. Reserve bitmap indexes exclusively for data warehouse tables that receive bulk loads during maintenance windows.\n\n### Mistake 6: Storing Calculated Fields Without Documentation\n\nStoring pre-calculated values (gross profit, discounts) is sometimes valid for performance, but without documenting the formula, discrepancies between source and derived values are inevitable. Use virtual columns where possible, or document the formula as a column comment.\n\n---\n\n## Security Considerations\n\n### Principle of Least Privilege in Schema Design\n- Design schemas with **minimal required privileges** for application users:\n ```sql\n -- Instead of granting broad access:\n -- GRANT SELECT ANY TABLE TO app_user; -- AVOID\n\n -- Grant specific object privileges:\n GRANT SELECT ON orders TO app_user;\n GRANT INSERT ON order_items TO app_user;\n GRANT UPDATE (status, shipped_date) ON orders TO app_user;\n ```\n- Separate schema owner from application user:\n ```sql\n -- Schema owner (locked account - never used for connections)\n CREATE USER app_schema IDENTIFIED BY \"strong_password\" ACCOUNT LOCK;\n\n -- Application service account (minimal privileges)\n CREATE USER app_user IDENTIFIED BY \"strong_password\";\n GRANT CREATE SESSION TO app_user;\n GRANT SELECT, INSERT, UPDATE ON app_schema.orders TO app_user;\n GRANT SELECT ON app_schema.customers TO app_user;\n ```\n\n### Data Protection Through Design\n- **Identify and isolate sensitive data** (PII, PCI, PHI) in separate tables/schemas:\n ```sql\n -- Isolate PII in separate table with stricter controls\n CREATE TABLE customer_pii (\n customer_id NUMBER PRIMARY KEY,\n ssn VARCHAR2(11), -- Will be encrypted\n dob DATE,\n passport_num VARCHAR2(20),\n -- Apply column encryption or use secure application logic\n );\n\n -- Reference PII table only when necessary\n CREATE TABLE customer_profile (\n customer_id NUMBER PRIMARY KEY,\n name VARCHAR2(100),\n email VARCHAR2(255),\n phone VARCHAR2(20),\n -- PII reference (foreign key)\n pii_id NUMBER REFERENCES customer_pii(customer_id)\n );\n ```\n- Use **Virtual Private Database (VPD)** for row-level security directly in the schema:\n ```sql\n -- Example: Tenant isolation in multi-tenant application\n CREATE OR REPLACE FUNCTION tenant_access_policy(\n p_schema VARCHAR2,\n p_object VARCHAR2\n ) RETURN VARCHAR2 AS\n BEGIN\n RETURN 'tenant_id = SYS_CONTEXT(''APP_CTX'', ''TENANT_ID'')';\n END;\n\n BEGIN\n DBMS_RLS.ADD_POLICY(\n object_schema => 'APP',\n object_name => 'ORDERS',\n policy_name => 'TENANT_ISOLATION_POLICY',\n function_schema => 'APP',\n policy_function => 'TENANT_ACCESS_POLICY',\n statement_types => 'SELECT,INSERT,UPDATE,DELETE'\n );\n END;\n ```\n\n### Secure Handling of Sensitive Data Types\n- **Encrypt sensitive columns** at the storage level:\n ```sql\n CREATE TABLE payments (\n payment_id NUMBER PRIMARY KEY,\n card_number VARCHAR2(19) ENCRYPT USING 'AES256', -- PCI PAN\n cvv VARCHAR2(4) ENCRYPT USING 'AES256', -- Never store CVV per PCI-DSS\n expiry_date DATE,\n amount NUMBER(10,2)\n );\n ```\n- **Never store CVV/CVC** - violates PCI-DSS requirement 3.2\n- Use **tokenization** for payment card data instead of storing actual PAN when possible\n\n### Auditing and Monitoring Considerations\n- Design tables with **audit columns** for forensic analysis:\n ```sql\n CREATE TABLE financial_transactions (\n transaction_id NUMBER PRIMARY KEY,\n account_id NUMBER NOT NULL,\n amount NUMBER(15,2) NOT NULL,\n transaction_type VARCHAR2(20) NOT NULL,\n -- Security audit columns\n created_by VARCHAR2(30) NOT NULL, -- Application user or service account\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n updated_by VARCHAR2(30),\n updated_at TIMESTAMP,\n -- For sensitive operations, consider:\n -- session_id VARCHAR2(30), -- From SYS_CONTEXT('USERENV','SESSIONID')\n -- ip_address VARCHAR2(45) -- From SYS_CONTEXT('USERENV','IP_ADDRESS')\n );\n ```\n- Enable **Fine-Grained Auditing (FGA)** on sensitive tables:\n ```sql\n BEGIN\n DBMS_FGA.ADD_POLICY(\n object_schema => 'FINANCE',\n object_name => 'WIRE_TRANSFERS',\n policy_name => 'MONITOR_LARGE_TRANSFERS',\n audit_column => 'AMOUNT',\n audit_condition => 'AMOUNT > 10000', -- Audit transfers over $10,000\n statement_types => 'INSERT,UPDATE'\n );\n END;\n ```\n\n### Input Validation and Injection Prevention\n- **Implement validation at the schema level** using constraints:\n ```sql\n CREATE TABLE users (\n user_id NUMBER PRIMARY KEY,\n username VARCHAR2(50) NOT NULL,\n email VARCHAR2(255)\n CONSTRAINT chk_email_format\n CHECK (REGEXP_LIKE(email, '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

, 'i')),\n phone VARCHAR2(20)\n CONSTRAINT chk_phone_format\n CHECK (REGEXP_LIKE(phone, '^\\+?[1-9]\\d{1,14}

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

)), -- E.164 format\n status VARCHAR2(20)\n CONSTRAINT chk_status\n CHECK (status IN ('ACTIVE', 'INACTIVE', 'SUSPENDED', 'CLOSED'))\n );\n ```\n- Use **check constraints** to enforce business rules and prevent invalid data\n- Consider **virtual columns** for derived values that should never be stored directly:\n ```sql\n CREATE TABLE orders (\n order_id NUMBER PRIMARY KEY,\n subtotal NUMBER(10,2) NOT NULL,\n tax_rate NUMBER(4,2) NOT NULL, -- Stored as percentage (e.g., 8.25 for 8.25%)\n tax_amount NUMBER(10,2) GENERATED ALWAYS AS (ROUND(subtotal * tax_rate / 100, 2)) VIRTUAL,\n shipping_cost NUMBER(8,2),\n total_amount NUMBER(10,2) GENERATED ALWAYS AS (subtotal + tax_amount + shipping_cost) VIRTUAL\n );\n ```\n\n### Secure Schema Evolution\n- **Use edition-based redefinition** for zero-downtime schema upgrades:\n ```sql\n -- Enable editions for schema\n ALTER USER app_schema ENABLE EDITIONS;\n\n -- Create edition for upgrade\n CREATE EDITION v2 AS CHILD OF ORA$BASE;\n\n -- Make edition available for use\n ALTER DATABASE DEFAULT EDITION = v2;\n ```\n- Implement **backward-compatible changes** when possible:\n ```sql\n -- ADD column with DEFAULT (12c+ feature - avoids table lock)\n ALTER TABLE orders ADD (\n discount_code VARCHAR2(20) DEFAULT NULL\n );\n\n -- For earlier versions, use nullable columns and application-level defaults\n ```\n\n### Compliance-Driven Design\n- **Design for data minimization** (GDPR Article 5(1)(c)):\n - Only collect data necessary for specified purpose\n - Implement automated purging/archive strategies:\n ```sql\n -- Example: Archive old transaction data\n CREATE TABLE transactions_archive AS\n SELECT * FROM transactions\n WHERE transaction_date \u003c ADD_MONTHS(SYSDATE, -24); -- Older than 2 years\n\n DELETE FROM transactions\n WHERE transaction_date \u003c ADD_MONTHS(SYSDATE, -24);\n ```\n- **Implement right to erasure** (GDPR Article 17):\n ```sql\n -- Procedure to anonymize/delete user data\n CREATE OR REPLACE PROCEDURE anonymize_user_data(p_user_id NUMBER) AS\n BEGIN\n -- Option 1: Anonymize (retain analytics value)\n UPDATE users SET\n email = 'anon_' || user_id || '@example.com',\n first_name = 'ANONYMIZED',\n last_name = 'USER',\n phone = NULL\n WHERE user_id = p_user_id;\n\n -- Option 2: Delete (when legally required)\n -- DELETE FROM user_related_tables WHERE user_id = p_user_id;\n -- DELETE FROM users WHERE user_id = p_user_id;\n\n COMMIT;\n END;\n ```\n- **Design for data portability** (GDPR Article 20):\n - Structure data to enable easy export in standard formats (JSON, XML, CSV)\n - Consider using Oracle's JSON relational duality views for flexible export\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 23ai Concepts Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/cncpt/)\n- [Oracle Database 23ai SQL Language Reference — CREATE TABLE](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/CREATE-TABLE.html)\n- [Oracle Database 23ai VLDB and Partitioning Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vldbg/)\n- [Oracle Advanced Compression FAQ (oracle.com)](https://www.oracle.com/a/ocom/docs/database/advanced-compression-faq.pdf)\n- [Oracle Exadata Hybrid Columnar Compression (docs.oracle.com)](https://docs.oracle.com/en/engineered-systems/exadata-database-machine/sagug/exadata-hybrid-columnar-compression.html)\n- [Oracle Database 12c R1 — In-Memory Column Store (oracle-base.com)](https://oracle-base.com/articles/12c/in-memory-column-store-12cr1)\n- [Oracle Database 19c — Enabling Objects for In-Memory Population](https://docs.oracle.com/en/database/oracle/oracle-database/19/inmem/populating-objects-in-memory.html)\n- [Oracle Database 23ai PL/SQL Packages and Types Reference — DBMS_SPACE](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/DBMS_SPACE.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":31255,"content_sha256":"b10b3e39281f2a1a1a7f93dcd9b626ca7700fa287f2d1a86da77588598ad1445"},{"filename":"design/erd-design.md","content":"# ERD Design & Normalization for Oracle Database\n\n## Overview\n\nEntity Relationship Design (ERD) is the foundational step in building a well-structured relational database. In Oracle environments, a well-designed ERD translates directly into maintainable schemas, efficient queries, and predictable growth. This guide covers the full spectrum of ERD design: entity and relationship modeling, normalization through all normal forms, Oracle-specific naming rules, and cardinality modeling best practices.\n\n---\n\n## 1. Core ERD Concepts\n\n### Entities\n\nAn **entity** represents a distinct object or concept about which data is stored. In Oracle, each entity typically maps to a table. Entities fall into two categories:\n\n- **Strong entities**: Exist independently (e.g., `CUSTOMER`, `PRODUCT`).\n- **Weak entities**: Depend on a strong entity for their existence (e.g., `ORDER_ITEM` depends on `ORDER`).\n\n### Attributes\n\nAttributes describe properties of an entity. Oracle-specific considerations:\n\n| Attribute Type | Description | Oracle Mapping |\n|---|---|---|\n| Simple | Single-valued, atomic | Standard column |\n| Composite | Can be broken into sub-parts (e.g., full name) | Multiple columns preferred |\n| Derived | Computed from other data | Virtual column or view |\n| Multi-valued | Can hold multiple values | Child table (avoid arrays) |\n\n### Relationships\n\nRelationships define how entities associate with one another. Oracle enforces relationships through **foreign key constraints**, **check constraints**, and **triggers**.\n\n---\n\n## 2. Relationship Cardinality\n\nCardinality defines the numeric nature of a relationship between two entities.\n\n### One-to-One (1:1)\n\nRare in practice; often indicates a candidate for table merging or a security-based split.\n\n```sql\nCREATE TABLE EMPLOYEE (\n employee_id NUMBER(10) NOT NULL,\n full_name VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_employee PRIMARY KEY (employee_id)\n);\n\nCREATE TABLE EMPLOYEE_SECURITY (\n employee_id NUMBER(10) NOT NULL,\n password_hash VARCHAR2(256) NOT NULL,\n last_login TIMESTAMP,\n CONSTRAINT pk_emp_security PRIMARY KEY (employee_id),\n CONSTRAINT fk_emp_security FOREIGN KEY (employee_id)\n REFERENCES EMPLOYEE (employee_id)\n ON DELETE CASCADE\n);\n```\n\n### One-to-Many (1:N)\n\nThe most common relationship type. The \"many\" side holds the foreign key.\n\n```sql\nCREATE TABLE DEPARTMENT (\n department_id NUMBER(6) NOT NULL,\n department_name VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_department PRIMARY KEY (department_id)\n);\n\nCREATE TABLE EMPLOYEE (\n employee_id NUMBER(10) NOT NULL,\n full_name VARCHAR2(100) NOT NULL,\n department_id NUMBER(6) NOT NULL,\n hire_date DATE NOT NULL,\n CONSTRAINT pk_employee PRIMARY KEY (employee_id),\n CONSTRAINT fk_emp_dept FOREIGN KEY (department_id)\n REFERENCES DEPARTMENT (department_id)\n);\n```\n\n### Many-to-Many (M:N)\n\nResolved through an **associative (junction) table**, which itself often carries attributes.\n\n```sql\nCREATE TABLE STUDENT (\n student_id NUMBER(10) NOT NULL,\n full_name VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_student PRIMARY KEY (student_id)\n);\n\nCREATE TABLE COURSE (\n course_id NUMBER(6) NOT NULL,\n course_name VARCHAR2(200) NOT NULL,\n CONSTRAINT pk_course PRIMARY KEY (course_id)\n);\n\n-- Junction table with its own attributes\nCREATE TABLE ENROLLMENT (\n student_id NUMBER(10) NOT NULL,\n course_id NUMBER(6) NOT NULL,\n enrolled_date DATE NOT NULL,\n grade VARCHAR2(2),\n CONSTRAINT pk_enrollment PRIMARY KEY (student_id, course_id),\n CONSTRAINT fk_enroll_stu FOREIGN KEY (student_id) REFERENCES STUDENT (student_id),\n CONSTRAINT fk_enroll_crs FOREIGN KEY (course_id) REFERENCES COURSE (course_id)\n);\n```\n\n### Self-Referencing (Recursive) Relationships\n\nCommon for hierarchical data (org charts, bill of materials).\n\n```sql\nCREATE TABLE CATEGORY (\n category_id NUMBER(10) NOT NULL,\n category_name VARCHAR2(100) NOT NULL,\n parent_category_id NUMBER(10), -- NULL for root nodes\n CONSTRAINT pk_category PRIMARY KEY (category_id),\n CONSTRAINT fk_cat_parent FOREIGN KEY (parent_category_id)\n REFERENCES CATEGORY (category_id)\n);\n```\n\nTraversing this hierarchy in Oracle uses `CONNECT BY` or recursive CTEs (11g R2+):\n\n```sql\n-- Oracle hierarchical query\nSELECT category_id, category_name, LEVEL AS depth,\n SYS_CONNECT_BY_PATH(category_name, ' > ') AS full_path\nFROM CATEGORY\nSTART WITH parent_category_id IS NULL\nCONNECT BY PRIOR category_id = parent_category_id\nORDER SIBLINGS BY category_name;\n```\n\n---\n\n## 3. Normalization\n\nNormalization reduces data redundancy and improves data integrity by organizing data into well-structured tables. Each normal form builds on the previous.\n\n### First Normal Form (1NF)\n\n**Rules:**\n- All column values must be atomic (indivisible).\n- No repeating groups or arrays.\n- Each row must be uniquely identifiable (primary key exists).\n\n**Violation example:**\n\n```\nCUSTOMER(customer_id, name, phone1, phone2, phone3) -- repeating groups\nCUSTOMER(customer_id, name, \"555-1234, 555-5678\") -- non-atomic value\n```\n\n**1NF resolution:**\n\n```sql\nCREATE TABLE CUSTOMER (\n customer_id NUMBER(10) NOT NULL,\n full_name VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_customer PRIMARY KEY (customer_id)\n);\n\nCREATE TABLE CUSTOMER_PHONE (\n customer_id NUMBER(10) NOT NULL,\n phone_type VARCHAR2(20) NOT NULL, -- MOBILE, HOME, WORK\n phone_number VARCHAR2(20) NOT NULL,\n CONSTRAINT pk_cust_phone PRIMARY KEY (customer_id, phone_type),\n CONSTRAINT fk_cust_phone FOREIGN KEY (customer_id)\n REFERENCES CUSTOMER (customer_id)\n);\n```\n\n### Second Normal Form (2NF)\n\n**Rules:**\n- Must be in 1NF.\n- Every non-key attribute must be fully functionally dependent on the **entire** primary key (no partial dependencies — only relevant when PK is composite).\n\n**Violation example** (composite PK: `order_id + product_id`):\n\n```\nORDER_ITEM(order_id, product_id, quantity, product_name, product_price)\n-- product_name and product_price depend only on product_id, not the full key\n```\n\n**2NF resolution:**\n\n```sql\nCREATE TABLE PRODUCT (\n product_id NUMBER(10) NOT NULL,\n product_name VARCHAR2(200) NOT NULL,\n unit_price NUMBER(12,2) NOT NULL,\n CONSTRAINT pk_product PRIMARY KEY (product_id)\n);\n\nCREATE TABLE ORDER_ITEM (\n order_id NUMBER(10) NOT NULL,\n product_id NUMBER(10) NOT NULL,\n quantity NUMBER(8) NOT NULL,\n unit_price NUMBER(12,2) NOT NULL, -- captured at time of order (snapshot)\n CONSTRAINT pk_order_item PRIMARY KEY (order_id, product_id),\n CONSTRAINT fk_oi_product FOREIGN KEY (product_id) REFERENCES PRODUCT (product_id)\n);\n```\n\n### Third Normal Form (3NF)\n\n**Rules:**\n- Must be in 2NF.\n- No transitive dependencies: non-key attributes must not depend on other non-key attributes.\n\n**Violation example:**\n\n```\nEMPLOYEE(employee_id, name, department_id, department_name, department_location)\n-- department_name and department_location depend on department_id, not employee_id\n```\n\n**3NF resolution:** Extract `DEPARTMENT` as a separate table (already shown in earlier examples).\n\n### Boyce-Codd Normal Form (BCNF)\n\nA stricter version of 3NF. Every determinant must be a candidate key. Violations occur when a table has multiple overlapping candidate keys.\n\n```sql\n-- Violation: TEACHING(student, subject, teacher)\n-- where each teacher teaches only one subject, but a student can have multiple teachers\n-- Determinants: (student, subject) -> teacher AND (student, teacher) -> subject\n-- teacher -> subject (teacher is not a candidate key)\n\n-- Resolution: split into two tables\nCREATE TABLE TEACHER_SUBJECT (\n teacher_id NUMBER(10) NOT NULL,\n subject_id NUMBER(10) NOT NULL,\n CONSTRAINT pk_teacher_subj PRIMARY KEY (teacher_id), -- each teacher one subject\n CONSTRAINT fk_ts_subject FOREIGN KEY (subject_id) REFERENCES SUBJECT (subject_id)\n);\n\nCREATE TABLE STUDENT_TEACHER (\n student_id NUMBER(10) NOT NULL,\n teacher_id NUMBER(10) NOT NULL,\n CONSTRAINT pk_student_teacher PRIMARY KEY (student_id, teacher_id)\n);\n```\n\n### Fourth Normal Form (4NF)\n\n**Rules:**\n- Must be in BCNF.\n- No multi-valued dependencies (two or more independent multi-valued facts about an entity in the same table).\n\n**Violation:**\n\n```\nEMPLOYEE_SKILLS_LANGUAGES(employee_id, skill, language)\n-- skills and languages are independent multi-valued facts about an employee\n```\n\n**4NF resolution:**\n\n```sql\nCREATE TABLE EMPLOYEE_SKILL (\n employee_id NUMBER(10) NOT NULL,\n skill VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_emp_skill PRIMARY KEY (employee_id, skill)\n);\n\nCREATE TABLE EMPLOYEE_LANGUAGE (\n employee_id NUMBER(10) NOT NULL,\n language VARCHAR2(100) NOT NULL,\n CONSTRAINT pk_emp_lang PRIMARY KEY (employee_id, language)\n);\n```\n\n### Fifth Normal Form (5NF)\n\n**Rules:**\n- Must be in 4NF.\n- No join dependencies that are not implied by candidate keys (no lossless decomposition into smaller tables can represent the original table's semantics better).\n\n5NF is primarily theoretical and rarely encountered in practice. It applies when a fact can only be represented by the combination of three or more entities simultaneously.\n\n---\n\n## 4. Oracle Naming Conventions\n\nOracle has strict naming rules and reserved words that must be respected during ERD-to-DDL translation.\n\n### Hard Rules (Oracle Enforced)\n\n- Object names: **1–128 bytes** (Oracle 12.2+); **1–30 bytes** in earlier versions.\n- Must start with a letter (unless quoted).\n- Can contain: letters, digits, `_`, ` db — Skillopedia , `#`.\n- Case-insensitive **unless** double-quoted.\n- **Avoid double-quoting**: it creates case-sensitive names that require quoting everywhere.\n\n### Recommended Naming Standards\n\n| Object | Convention | Example |\n|---|---|---|\n| Table | Plural noun, UPPER_SNAKE_CASE | `CUSTOMERS`, `ORDER_ITEMS` |\n| Column | Descriptive noun, UPPER_SNAKE_CASE | `CUSTOMER_ID`, `CREATED_AT` |\n| Primary Key | `PK_\u003ctable>` | `PK_CUSTOMERS` |\n| Foreign Key | `FK_\u003cchild>_\u003cparent>` | `FK_ORDERS_CUSTOMERS` |\n| Unique Constraint | `UQ_\u003ctable>_\u003ccolumn(s)>` | `UQ_CUSTOMERS_EMAIL` |\n| Check Constraint | `CK_\u003ctable>_\u003ccolumn>` | `CK_EMPLOYEES_SALARY` |\n| Index | `IX_\u003ctable>_\u003ccolumn(s)>` | `IX_ORDERS_ORDER_DATE` |\n| Sequence | `SEQ_\u003ctable>` | `SEQ_CUSTOMERS` |\n| View | `V_\u003cname>` or `VW_\u003cname>` | `V_ACTIVE_ORDERS` |\n| Trigger | `TRG_\u003ctable>_\u003ctiming>_\u003cevent>` | `TRG_ORDERS_BI_INSERT` |\n\n### Oracle Reserved Words to Avoid as Identifiers\n\nThe following are commonly misused Oracle reserved words — never use these as table or column names without quoting:\n\n```\nACCESS ADD ALL ALTER AND\nANY AS ASC AUDIT BETWEEN\nBY CHAR CHECK CLUSTER COLUMN\nCOMMENT COMPRESS CONNECT CREATE CURRENT\nDATE DECIMAL DEFAULT DELETE DESC\nDISTINCT DROP ELSE EXCLUSIVE EXISTS\nFILE FLOAT FOR FROM GRANT\nGROUP HAVING IDENTIFIED IMMEDIATE IN\nINCREMENT INDEX INITIAL INSERT INTEGER\nINTERSECT INTO IS LEVEL LIKE\nLOCK LONG MAXEXTENTS MINUS MLSLABEL\nMODE MODIFY NOAUDIT NOCOMPRESS NOT\nNOWAIT NULL NUMBER OF OFFLINE\nON ONLINE OPTION OR ORDER\nPCTFREE PRIOR PRIVILEGES PUBLIC RAW\nRENAME RESOURCE REVOKE ROW ROWID\nROWNUM ROWS SELECT SESSION SET\nSHARE SIZE SMALLINT START SUCCESSFUL\nSYNONYM SYSDATE TABLE THEN TO\nTRIGGER UID UNION UNIQUE UPDATE\nUSER VALIDATE VALUES VARCHAR VARCHAR2\nVIEW WHENEVER WHERE WITH\n```\n\n**Problematic column name examples and replacements:**\n\n```sql\n-- BAD: uses reserved words\nCREATE TABLE ORDERS (\n order_id NUMBER,\n date DATE, -- DATE is a reserved word\n comment VARCHAR2(500) -- COMMENT is a reserved word\n);\n\n-- GOOD: descriptive alternatives\nCREATE TABLE ORDERS (\n order_id NUMBER,\n order_date DATE,\n order_comment VARCHAR2(500)\n);\n```\n\n---\n\n## 5. Oracle-Specific ERD Considerations\n\n### Surrogate vs. Natural Keys\n\nOracle strongly supports surrogate primary keys via **sequences** and **identity columns** (12c+).\n\n```sql\n-- Oracle 12c+ identity column (recommended for new development)\nCREATE TABLE CUSTOMER (\n customer_id NUMBER GENERATED ALWAYS AS IDENTITY,\n email VARCHAR2(255) NOT NULL,\n created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n CONSTRAINT pk_customer PRIMARY KEY (customer_id),\n CONSTRAINT uq_cust_email UNIQUE (email)\n);\n\n-- Pre-12c pattern using sequence + trigger\nCREATE SEQUENCE SEQ_CUSTOMER START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE;\n\nCREATE OR REPLACE TRIGGER TRG_CUSTOMER_BI\nBEFORE INSERT ON CUSTOMER\nFOR EACH ROW\nBEGIN\n IF :NEW.customer_id IS NULL THEN\n :NEW.customer_id := SEQ_CUSTOMER.NEXTVAL;\n END IF;\nEND;\n/\n```\n\n### Constraint Deferability\n\nOracle supports **deferrable constraints**, which is critical when dealing with circular foreign keys or bulk load operations.\n\n```sql\n-- Deferrable FK — useful for batch inserts where parent/child order is uncertain\nALTER TABLE ORDER_ITEM\n ADD CONSTRAINT fk_oi_order\n FOREIGN KEY (order_id) REFERENCES ORDERS (order_id)\n DEFERRABLE INITIALLY DEFERRED;\n```\n\n### Virtual Columns as Derived Attributes\n\nInstead of storing computed values, Oracle virtual columns keep data consistent without application logic.\n\n```sql\nCREATE TABLE PRODUCT (\n product_id NUMBER(10) NOT NULL,\n unit_price NUMBER(12,2) NOT NULL,\n tax_rate NUMBER(5,4) NOT NULL,\n price_with_tax NUMBER(12,2) GENERATED ALWAYS AS (unit_price * (1 + tax_rate)) VIRTUAL,\n CONSTRAINT pk_product PRIMARY KEY (product_id)\n);\n```\n\n### Invisible Columns (12c+)\n\nUseful during schema migrations — add new columns without breaking existing `SELECT *` queries.\n\n```sql\nALTER TABLE CUSTOMER ADD (\n legacy_system_id VARCHAR2(50) INVISIBLE\n);\n```\n\n---\n\n## 6. Best Practices\n\n- **Always define primary keys** on every table. Oracle will create a unique index automatically.\n- **Name all constraints explicitly.** Anonymous constraints receive system-generated names (e.g., `SYS_C001234`) that make maintenance, error messages, and migrations extremely difficult.\n- **Enforce NOT NULL at the database level**, not just the application layer, for mandatory attributes.\n- **Use `DATE` for date-only data and `TIMESTAMP WITH TIME ZONE` for datetime data** that crosses time zones. Avoid storing dates as `VARCHAR2`.\n- **Model optional relationships carefully.** A nullable foreign key is appropriate; a mandatory relationship should enforce `NOT NULL` on the FK column.\n- **Keep junction tables thin.** The junction table's purpose is to resolve the M:N — business attributes naturally appearing on the relationship (date, quantity, status) belong there, but avoid over-loading them.\n- **Document ERDs with business context.** Column comments in Oracle are part of the schema and are invaluable for future developers.\n\n```sql\nCOMMENT ON TABLE CUSTOMER IS 'Registered customers who have completed account creation.';\nCOMMENT ON COLUMN CUSTOMER.customer_id IS 'Surrogate primary key, generated by identity column.';\nCOMMENT ON COLUMN CUSTOMER.email IS 'Unique login email address. Lowercased before storage.';\n```\n\n---\n\n## 7. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Storing Multiple Values in a Single Column\n\n```sql\n-- BAD: CSV in a column\nCREATE TABLE PROJECT (\n project_id NUMBER,\n team_members VARCHAR2(4000) -- \"101,102,103\" — unqueryable, unmaintainable\n);\n\n-- GOOD: proper child table\nCREATE TABLE PROJECT_MEMBER (\n project_id NUMBER NOT NULL,\n employee_id NUMBER NOT NULL,\n CONSTRAINT pk_project_member PRIMARY KEY (project_id, employee_id)\n);\n```\n\n### Mistake 2: Using ROWNUM or ROWID as a Primary Key\n\n`ROWNUM` and `ROWID` are pseudo-columns — they change on bulk operations, partition moves, and table reorganizations. Always use a proper surrogate or natural key.\n\n### Mistake 3: Skipping Foreign Key Indexes\n\nOracle does **not** automatically create indexes on foreign key columns. Without these indexes, lock escalation and full table scans occur on the child table during parent-row deletes.\n\n```sql\n-- After creating the FK, always add an index on the FK column(s)\nCREATE INDEX IX_ORDER_ITEMS_ORDER_ID ON ORDER_ITEMS (order_id);\nCREATE INDEX IX_ORDER_ITEMS_PRODUCT_ID ON ORDER_ITEMS (product_id);\n```\n\n### Mistake 4: Over-Normalizing for Performance-Critical OLTP\n\nWhile 3NF is the target for OLTP, over-normalizing into dozens of tiny tables can create query-time joins that hurt performance. Profile before splitting further than 3NF.\n\n### Mistake 5: Ignoring NULL Semantics in Unique Constraints\n\nOracle treats `NULL` as distinct from all values **including other NULLs** in unique constraints. Multiple rows can have `NULL` in a unique-constrained column. If you need \"unique or null\" behavior with explicit NULL handling, use a unique function-based index.\n\n```sql\n-- Allow multiple NULLs but enforce uniqueness for non-NULL values\nCREATE UNIQUE INDEX UX_EMP_NATIONAL_ID\n ON EMPLOYEE (CASE WHEN national_id IS NOT NULL THEN national_id END);\n```\n\n### Mistake 6: Using VARCHAR2 for Fixed-Format Codes\n\nUse `CHAR` for truly fixed-length codes (ISO country codes, state abbreviations) to save storage and ensure consistent comparisons.\n\n```sql\ncountry_code CHAR(2) NOT NULL, -- 'US', 'GB', 'AU'\nstatus_code CHAR(1) NOT NULL -- 'A'ctive, 'I'nactive, 'S'uspended\n```\n\n---\n\n## 8. Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features listed from 21c/23c generations are Oracle Database 26ai-capable; keep 19c alternatives for mixed-version estates.\n- Validate defaults and behavior in your exact RU level when running both 19c and 26ai.\n\n| Feature | Version Introduced |\n|---|---|\n| Identity columns | 12c (12.1) |\n| Invisible columns | 12c (12.1) |\n| In-memory column store | 12c (12.1.0.2 patchset) |\n| Polymorphic table functions | 18c |\n| Automatic indexing | 19c |\n| Blockchain tables | 21c (20c was preview only; backported to 19.10+) |\n| Object name length 128 bytes | 12.2 |\n| `WITH FUNCTION` inline SQL functions | 12c (12.1) |\n\nFor pre-12c systems, replace identity columns with sequences and before-insert triggers, and keep object names to 30 characters maximum.\n\n---\n\n## Sources\n\n- [Oracle Database 23ai SQL Language Reference — Database Object Names and Qualifiers](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Database-Object-Names-and-Qualifiers.html)\n- [Oracle Database 23ai Concepts — Tables and Table Clusters](https://docs.oracle.com/en/database/oracle/oracle-database/23/cncpt/tables-and-table-clusters.html)\n- [Oracle Database 12c R1 — Identity Columns (oracle-base.com)](https://oracle-base.com/articles/12c/identity-columns-in-oracle-12cr1)\n- [Oracle Database 12c R1 — Invisible Columns (oracle-base.com)](https://oracle-base.com/articles/12c/invisible-columns-12cr1)\n- [Oracle Database 12c R1 — In-Memory Column Store (oracle-base.com)](https://oracle-base.com/articles/12c/in-memory-column-store-12cr1)\n- [Oracle Database 12c R1 — WITH Clause Enhancements (oracle-base.com)](https://oracle-base.com/articles/12c/with-clause-enhancements-12cr1)\n- [Oracle Database 18c — Polymorphic Table Functions (oracle-base.com)](https://oracle-base.com/articles/18c/polymorphic-table-functions-18c)\n- [Oracle Database 19c — Automatic Indexing (oracle-base.com)](https://oracle-base.com/articles/19c/automatic-indexing-19c)\n- [Oracle Database 21c — Blockchain Tables (oracle-base.com)](https://oracle-base.com/articles/21c/blockchain-tables-21c)\n- [Oracle Database 11g R2 — Recursive Subquery Factoring (oracle-base.com)](https://oracle-base.com/articles/11g/recursive-subquery-factoring-11gr2)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20366,"content_sha256":"24b6ee77104c462cf7e152c53241bfb92f95ed8265fa97c9d63760098c5714d0"},{"filename":"design/partitioning-strategy.md","content":"# Oracle Partitioning Strategy\n\n## Overview\n\nOracle Table Partitioning divides large tables and indexes into smaller, independently manageable segments called **partitions**. Each partition is a separate physical segment but is accessed transparently through the table name. Partitioning delivers three primary benefits:\n\n1. **Partition Pruning**: The optimizer eliminates irrelevant partitions from query plans, dramatically reducing I/O.\n2. **Partition-wise Operations**: Parallel operations (joins, aggregations) can be divided along partition boundaries.\n3. **Partition Maintenance**: Archive, purge, or move individual partitions as atomic DDL operations — far faster than row-level deletes.\n\nPartitioning requires the **Oracle Partitioning option** (Enterprise Edition). Verify availability:\n\n```sql\nSELECT value FROM v$option WHERE parameter = 'Partitioning';\n```\n\n---\n\n## 1. Range Partitioning\n\n**Range partitioning** assigns rows to partitions based on a column value falling within a specified range. It is the most common partitioning type and is ideal for time-series data (dates, timestamps, sequential IDs).\n\n### Syntax\n\n```sql\nCREATE TABLE SALES (\n sale_id NUMBER NOT NULL,\n sale_date DATE NOT NULL,\n customer_id NUMBER NOT NULL,\n product_id NUMBER NOT NULL,\n amount NUMBER(14,2) NOT NULL,\n CONSTRAINT pk_sales PRIMARY KEY (sale_id, sale_date) -- PK must include partition key\n)\nTABLESPACE sales_data\nCOMPRESS FOR QUERY HIGH\nPARTITION BY RANGE (sale_date) (\n PARTITION p_2022_q1 VALUES LESS THAN (DATE '2022-04-01') TABLESPACE sales_2022,\n PARTITION p_2022_q2 VALUES LESS THAN (DATE '2022-07-01') TABLESPACE sales_2022,\n PARTITION p_2022_q3 VALUES LESS THAN (DATE '2022-10-01') TABLESPACE sales_2022,\n PARTITION p_2022_q4 VALUES LESS THAN (DATE '2023-01-01') TABLESPACE sales_2022,\n PARTITION p_2023_q1 VALUES LESS THAN (DATE '2023-04-01') TABLESPACE sales_2023,\n PARTITION p_future VALUES LESS THAN (MAXVALUE) TABLESPACE sales_data\n);\n```\n\n### Interval Partitioning (Range Extension)\n\nOracle 11g+ supports **interval partitioning**, which automatically creates new partitions as data is inserted beyond existing boundaries — eliminating the need to manually add partitions.\n\n```sql\nCREATE TABLE ORDERS (\n order_id NUMBER NOT NULL,\n order_date DATE NOT NULL,\n customer_id NUMBER NOT NULL,\n total NUMBER(14,2) NOT NULL,\n CONSTRAINT pk_orders PRIMARY KEY (order_id, order_date)\n)\nTABLESPACE orders_data\nPARTITION BY RANGE (order_date)\nINTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) -- auto-create one partition per month\n(\n -- At least one partition must be defined manually as the seed\n PARTITION p_before_2023 VALUES LESS THAN (DATE '2023-01-01')\n TABLESPACE orders_archive\n);\n```\n\nFor interval partitions, specify the tablespace via `SET STORE IN`:\n\n```sql\nCREATE TABLE LOG_EVENTS (\n event_id NUMBER NOT NULL,\n event_time TIMESTAMP NOT NULL,\n severity VARCHAR2(10),\n message VARCHAR2(4000)\n)\nPARTITION BY RANGE (event_time)\nINTERVAL (INTERVAL '1' DAY)\nSTORE IN (log_ts_1, log_ts_2, log_ts_3) -- round-robin across tablespaces\n(\n PARTITION p_before_start VALUES LESS THAN (TIMESTAMP '2023-01-01 00:00:00')\n);\n```\n\n### Range Partition Pruning Example\n\n```sql\n-- This query prunes to p_2022_q1 and p_2022_q2 only\nSELECT sale_id, amount\nFROM SALES\nWHERE sale_date BETWEEN DATE '2022-01-01' AND DATE '2022-06-30';\n\n-- Verify pruning in execution plan\nEXPLAIN PLAN FOR\nSELECT sale_id, amount FROM SALES\nWHERE sale_date BETWEEN DATE '2022-01-01' AND DATE '2022-06-30';\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'PARTITION'));\n-- Look for \"Pstart\" and \"Pstop\" columns in the plan output\n```\n\n---\n\n## 2. List Partitioning\n\n**List partitioning** assigns rows to partitions based on discrete, enumerated column values. It is ideal for categorical data such as region, status, country code, or business unit.\n\n### Syntax\n\n```sql\nCREATE TABLE CUSTOMERS (\n customer_id NUMBER NOT NULL,\n region VARCHAR2(20) NOT NULL, -- partition key\n full_name VARCHAR2(200) NOT NULL,\n email VARCHAR2(255) NOT NULL,\n CONSTRAINT pk_customers PRIMARY KEY (customer_id, region)\n)\nPARTITION BY LIST (region) (\n PARTITION p_north_america VALUES ('US', 'CA', 'MX'),\n PARTITION p_europe VALUES ('GB', 'DE', 'FR', 'IT', 'ES', 'NL'),\n PARTITION p_apac VALUES ('AU', 'JP', 'SG', 'IN', 'CN', 'KR'),\n PARTITION p_latam VALUES ('BR', 'AR', 'CL', 'CO'),\n PARTITION p_other VALUES (DEFAULT) -- catch-all (11g+: DEFAULT keyword)\n);\n```\n\n**Note:** Without a `DEFAULT` partition in Oracle 11g+, inserting an unmapped value raises `ORA-14400: inserted partition key does not map to any partition`. Always include a `DEFAULT` or `MAXVALUE` catch-all.\n\n### Automatic List Partitioning (Oracle 12c R2+)\n\n```sql\nCREATE TABLE TRANSACTIONS (\n txn_id NUMBER NOT NULL,\n payment_type VARCHAR2(30) NOT NULL,\n amount NUMBER(14,2) NOT NULL\n)\nPARTITION BY LIST (payment_type) AUTOMATIC -- new partitions created on-the-fly\n(\n PARTITION p_credit_card VALUES ('VISA', 'MASTERCARD', 'AMEX')\n);\n-- First insert of 'PAYPAL' automatically creates partition p_sys_p_xxx\n```\n\n---\n\n## 3. Hash Partitioning\n\n**Hash partitioning** distributes rows evenly across a fixed number of partitions using an Oracle-internal hash function applied to the partition key. It does **not** support partition pruning by value range or list — it is used purely for **I/O distribution** and **parallel join performance**.\n\n### When to Use Hash Partitioning\n\n- No natural range or category boundary exists\n- The goal is even I/O distribution across storage devices\n- Enabling **partition-wise joins** (when joining two hash-partitioned tables on the same key with the same number of partitions)\n- Reducing hot-block contention on high-concurrency insert tables or indexes\n\n### Syntax\n\n```sql\n-- Power-of-2 partition counts are common, but not required\nCREATE TABLE SESSION_EVENTS (\n event_id NUMBER NOT NULL,\n session_id VARCHAR2(64) NOT NULL, -- hash key: evenly distributed\n event_type VARCHAR2(50) NOT NULL,\n event_time TIMESTAMP NOT NULL,\n payload CLOB,\n CONSTRAINT pk_session_events PRIMARY KEY (event_id)\n)\nPARTITION BY HASH (session_id)\nPARTITIONS 16\nSTORE IN (hash_ts_1, hash_ts_2, hash_ts_3, hash_ts_4); -- 16 partitions distributed across 4 tablespaces\n```\n\n### Partition-Wise Join (Hash Partitioning Benefit)\n\n```sql\n-- Both tables hash-partitioned on the same key with same partition count\nCREATE TABLE ORDERS_H (\n order_id NUMBER NOT NULL,\n customer_id NUMBER NOT NULL -- hash partition key\n)\nPARTITION BY HASH (customer_id) PARTITIONS 32;\n\nCREATE TABLE ORDER_ITEMS_H (\n item_id NUMBER NOT NULL,\n order_id NUMBER NOT NULL,\n customer_id NUMBER NOT NULL -- same hash partition key\n)\nPARTITION BY HASH (customer_id) PARTITIONS 32;\n\n-- Oracle can perform full partition-wise join: each partition pair joined independently\nSELECT /*+ PQ_DISTRIBUTE(i PARTITION NONE) */ o.order_id, i.item_id\nFROM ORDERS_H o\nJOIN ORDER_ITEMS_H i ON o.customer_id = i.customer_id AND o.order_id = i.order_id;\n```\n\n---\n\n## 4. Composite Partitioning\n\n**Composite partitioning** combines two partitioning strategies: a primary strategy (the partition level) and a secondary strategy (the subpartition level). This enables both partition pruning on the top key and even distribution or categorization within each partition.\n\n### Range-Hash Composite\n\nBest for time-series data that also needs even I/O distribution within each time period.\n\n```sql\nCREATE TABLE CLICKSTREAM (\n event_id NUMBER NOT NULL,\n event_date DATE NOT NULL, -- range partition key\n user_id NUMBER NOT NULL, -- hash subpartition key\n page_url VARCHAR2(2000),\n event_type VARCHAR2(50),\n CONSTRAINT pk_clickstream PRIMARY KEY (event_id, event_date)\n)\nPARTITION BY RANGE (event_date)\nSUBPARTITION BY HASH (user_id) SUBPARTITIONS 8\n(\n PARTITION p_2023_q1 VALUES LESS THAN (DATE '2023-04-01')\n STORE IN (cs_ts_1, cs_ts_2, cs_ts_3, cs_ts_4),\n PARTITION p_2023_q2 VALUES LESS THAN (DATE '2023-07-01')\n STORE IN (cs_ts_1, cs_ts_2, cs_ts_3, cs_ts_4),\n PARTITION p_2023_q3 VALUES LESS THAN (DATE '2023-10-01'),\n PARTITION p_future VALUES LESS THAN (MAXVALUE)\n);\n```\n\n### Range-List Composite\n\nBest for time-series data that also needs region/category segmentation.\n\n```sql\n-- Subpartition template: apply consistent subpartition structure to all range partitions\nCREATE TABLE REGIONAL_SALES (\n sale_id NUMBER NOT NULL,\n sale_date DATE NOT NULL,\n region VARCHAR2(20) NOT NULL,\n amount NUMBER(14,2) NOT NULL,\n CONSTRAINT pk_reg_sales PRIMARY KEY (sale_id, sale_date, region)\n)\nPARTITION BY RANGE (sale_date)\nSUBPARTITION BY LIST (region)\nSUBPARTITION TEMPLATE ( -- template applies to all range partitions\n SUBPARTITION sp_na VALUES ('US', 'CA', 'MX'),\n SUBPARTITION sp_eu VALUES ('GB', 'DE', 'FR'),\n SUBPARTITION sp_apac VALUES ('AU', 'JP', 'SG'),\n SUBPARTITION sp_other VALUES (DEFAULT)\n)\n(\n PARTITION p_2023 VALUES LESS THAN (DATE '2024-01-01'),\n PARTITION p_2024 VALUES LESS THAN (DATE '2025-01-01'),\n PARTITION p_future VALUES LESS THAN (MAXVALUE)\n);\n\n-- Pruning works on both levels simultaneously\nSELECT * FROM REGIONAL_SALES\nWHERE sale_date >= DATE '2023-01-01'\nAND sale_date \u003c DATE '2024-01-01'\nAND region IN ('US', 'CA');\n-- Prunes to p_2023 partition, sp_na subpartition only\n```\n\n### List-Hash Composite\n\nBest for categorical top-level segmentation with even distribution within each category.\n\n```sql\nCREATE TABLE PRODUCT_INVENTORY (\n inventory_id NUMBER NOT NULL,\n warehouse_id NUMBER NOT NULL, -- hash subpartition key\n category VARCHAR2(50) NOT NULL, -- list partition key\n product_id NUMBER NOT NULL,\n quantity NUMBER NOT NULL\n)\nPARTITION BY LIST (category)\nSUBPARTITION BY HASH (warehouse_id) SUBPARTITIONS 4\n(\n PARTITION p_electronics VALUES ('PHONES', 'LAPTOPS', 'TABLETS'),\n PARTITION p_clothing VALUES ('SHIRTS', 'PANTS', 'SHOES'),\n PARTITION p_food VALUES ('FRESH', 'FROZEN', 'DRY'),\n PARTITION p_other VALUES (DEFAULT)\n);\n```\n\n---\n\n## 5. Partition Pruning\n\nPartition pruning is the optimizer's ability to exclude irrelevant partitions from execution plans. It is the primary performance benefit of partitioning and only works when the partition key is included in the `WHERE` clause with a **static or bind-variable** predicate.\n\n### Pruning Requirements\n\n```sql\n-- GOOD: Static literal — compile-time pruning\nSELECT * FROM ORDERS WHERE order_date >= DATE '2024-01-01';\n\n-- GOOD: Bind variable — runtime pruning (still efficient)\nSELECT * FROM ORDERS WHERE order_date >= :start_date;\n\n-- BAD: Function applied to partition key — disables pruning\nSELECT * FROM ORDERS WHERE TRUNC(order_date) >= DATE '2024-01-01';\n-- Fix: store a pre-truncated date column, or use: order_date >= DATE '2024-01-01'\n-- AND order_date \u003c DATE '2024-01-02'\n\n-- BAD: Implicit type conversion — may disable pruning\nSELECT * FROM ORDERS WHERE order_date >= '2024-01-01'; -- string comparison\n-- Fix: always use explicit DATE literals or TO_DATE()\n```\n\n### Verifying Pruning in Execution Plans\n\n```sql\n-- Method 1: DBMS_XPLAN with PARTITION format\nEXPLAIN PLAN FOR\nSELECT * FROM SALES WHERE sale_date BETWEEN DATE '2024-01-01' AND DATE '2024-03-31';\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'ALL'));\n-- Key output: look for \"Pstart=1 Pstop=1\" (single partition) vs \"Pstart=1 Pstop=4\"\n\n-- Method 2: V$SQL_PLAN for running queries\nSELECT partition_start, partition_stop, partition_id, operation, options\nFROM v$sql_plan\nWHERE sql_id = :sql_id\nORDER BY id;\n```\n\n### Partition Elimination Statistics\n\n```sql\n-- Check partition-level statistics for a table\nSELECT table_name, partition_name, num_rows, blocks, last_analyzed\nFROM user_tab_partitions\nWHERE table_name = 'SALES'\nORDER BY partition_position;\n```\n\n---\n\n## 6. Local vs Global Indexes\n\n### Local Indexes\n\nA **local index** is partitioned using the same strategy as the underlying table. Each index partition corresponds to exactly one table partition. Local indexes are the preferred choice for partitioned tables if there is a requirement to keep partition maintenance as inexpensive as possible.\n\n```sql\n-- Local non-unique index\nCREATE INDEX IX_SALES_CUSTOMER ON SALES (customer_id)\nLOCAL -- one index partition per table partition\nTABLESPACE sales_idx;\n\n-- Local unique index (partition key must be included for uniqueness guarantee)\nCREATE UNIQUE INDEX UX_SALES_ORDER_LINE ON SALES (order_id, line_id, sale_date)\nLOCAL\nTABLESPACE sales_idx;\n```\n\n**Local index characteristics:**\n- Automatically maintained during partition operations (split, merge, drop, exchange)\n- Partition pruning on the table automatically prunes the index too\n- `UNUSABLE` state is limited to the affected partition, not the whole index\n- Unique local indexes require the partition key to be part of the unique key\n\n### Global Indexes\n\nA **global index** spans all table partitions as a single index structure. It provides uniqueness enforcement independent of partition key.\n\n```sql\n-- Global partitioned index (range-partitioned on the index key)\nCREATE UNIQUE INDEX UX_ORDERS_ORDER_NUM ON ORDERS (order_number)\nGLOBAL\nPARTITION BY RANGE (order_number) (\n PARTITION p_low VALUES LESS THAN (1000000),\n PARTITION p_mid VALUES LESS THAN (9000000),\n PARTITION p_high VALUES LESS THAN (MAXVALUE)\n)\nTABLESPACE orders_idx;\n\n-- Global non-partitioned index (classic single-structure index spanning all partitions)\nCREATE INDEX IX_ORDERS_EMAIL ON ORDERS (customer_email)\nGLOBAL -- or simply omit GLOBAL/LOCAL — non-partitioned is the default\nTABLESPACE orders_idx;\n```\n\n**Global index characteristics:**\n- Allows unique constraints on columns that don't include the partition key\n- Become stale after partition maintenance operations unless `UPDATE GLOBAL INDEXES` is specified. Once stale, the index can be manually rebuilt or the database automatically correct state global indexes over time with the back ground job\n- Can be more expensive to maintain during partition DDL if `UPDATE GLOBAL INDEXES` is specified\n\n```sql\n-- Partition maintenance with global index update\nALTER TABLE ORDERS DROP PARTITION p_2020\nUPDATE GLOBAL INDEXES; -- keeps global indexes valid; slower but avoids rebuilds\n\n-- Or let global indexes go UNUSABLE (default when no index clause is specified) then rebuild\n-- NOTE: there is no INVALIDATE GLOBAL INDEXES clause; omitting the clause causes indexes to become UNUSABLE\nALTER TABLE ORDERS DROP PARTITION p_2020;\nALTER INDEX UX_ORDERS_ORDER_NUM REBUILD;\n```\n\n### Local vs Global Decision Matrix\n\n| Criterion | Local Index | Global Index |\n|---|---|---|\n| Partition maintenance impact | Minimal (only affected partition) | Full rebuild required (unless `UPDATE GLOBAL INDEXES`) |\n| Partition pruning support | Full pruning on both table and index | Index-level pruning only (if global is partitioned) |\n| Unique constraint without partition key | Not possible | Supported |\n| Range scan on non-partition key | Less efficient | More efficient |\n| Best for | DW fact tables, time-series data | OLTP unique lookups, cross-partition range scans |\n\n---\n\n## 7. Partition Maintenance Operations\n\n```sql\n-- Add a new partition to a range-partitioned table\nALTER TABLE SALES ADD PARTITION p_2025_q1\n VALUES LESS THAN (DATE '2025-04-01')\n TABLESPACE sales_2025;\n\n-- Drop an old partition (and its data!) — irreversible\nALTER TABLE SALES DROP PARTITION p_2022_q1\nUPDATE GLOBAL INDEXES;\n\n-- Truncate a partition (delete all rows in partition — faster than DELETE)\nALTER TABLE SALES TRUNCATE PARTITION p_2022_q1;\n\n-- Exchange a partition with a staging table (zero-copy ETL)\n-- 1. Create staging table with identical structure (no partitioning)\nCREATE TABLE SALES_STAGING FOR EXCHANGE WITH SALES;\n\n-- 2. Load data into staging table (bulk insert, external table, etc.)\nINSERT /*+ APPEND */ INTO SALES_STAGING SELECT ...;\nCOMMIT;\n\n-- 3. Exchange: swap partition with staging table atomically\nALTER TABLE SALES EXCHANGE PARTITION p_2024_q4\nWITH TABLE SALES_STAGING\nINCLUDING INDEXES\nWITHOUT VALIDATION; -- skip row validation for performance\n\n-- Split a partition into two\nALTER TABLE SALES SPLIT PARTITION p_future\n AT (DATE '2026-01-01')\n INTO (PARTITION p_2025_q4, PARTITION p_future)\nUPDATE GLOBAL INDEXES;\n\n-- Merge two adjacent partitions\nALTER TABLE SALES MERGE PARTITIONS p_2022_q1, p_2022_q2\n INTO PARTITION p_2022_h1\nUPDATE GLOBAL INDEXES;\n```\n\n---\n\n## 8. When to Use Each Partitioning Type\n\n| Partitioning Type | Best Use Cases | Avoid When |\n|---|---|---|\n| **Range** | Time-series data, log tables, historical archives; enables rolling window (drop oldest, add newest) | Data has no natural range ordering |\n| **Interval (Range extension)** | Same as range but with automatic partition creation; high-volume append tables | You need precise control over partition creation timing |\n| **List** | Regional data, status codes, business unit data; values are discrete and enumerable | Too many distinct values; values change frequently |\n| **Automatic List** | Same as list but unknown future values; multi-tenant schemas | You need predictable partition names and locations |\n| **Hash** | Even I/O distribution; eliminate hot spots; partition-wise joins | You need partition pruning by value; sequential scans by key |\n| **Range-Hash** | Time-series data needing even distribution within time windows | Simple time-series without concurrency issues |\n| **Range-List** | Time-series data needing regional/categorical segmentation | The list categories are unstable or too numerous |\n| **List-Hash** | Categorical data needing even sub-distribution | Simple categorical data without skew |\n\n---\n\n## 9. Best Practices\n\n- **Always include the partition key in primary and unique keys** for local unique indexes. Oracle enforces this requirement.\n- **Choose partition granularity based on data volume and pruning patterns.** Monthly partitions for tables receiving ~10M rows/month; quarterly or annual partitions for smaller tables.\n- **Use `INTERVAL` partitioning** for any append-only time-series table to eliminate partition maintenance overhead.\n- **Gather partition-level statistics.** The optimizer needs current stats per partition for accurate cardinality estimates.\n ```sql\n EXEC DBMS_STATS.GATHER_TABLE_STATS(\n ownname => 'SCHEMA_OWNER',\n tabname => 'SALES',\n partname => 'P_2024_Q4',\n granularity => 'PARTITION',\n estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE\n );\n ```\n- **Use `COMPRESS FOR QUERY HIGH`** on all data warehouse fact table partitions. Apply on a per-partition basis to avoid recompressing historical data.\n- **Create local indexes for DW tables; use global indexes sparingly** and only where cross-partition uniqueness or non-partition-key range scans are required.\n\n---\n\n## 10. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Applying Functions to the Partition Key in WHERE Clauses\n\n```sql\n-- BAD: TRUNC() defeats partition pruning\nSELECT * FROM ORDERS WHERE TRUNC(order_date, 'MM') = DATE '2024-01-01';\n\n-- GOOD: use a range predicate directly on the partition key\nSELECT * FROM ORDERS\nWHERE order_date >= DATE '2024-01-01'\nAND order_date \u003c DATE '2024-02-01';\n```\n\n### Mistake 2: Non-Power-of-2 Hash Partition Count\n\nHash partition counts do not need to be powers of 2. Use a partition count that matches operational needs, expected data volume, and desired parallelism. Power-of-2 counts are a common convention, not a requirement.\n\n```sql\n-- Acceptable\nPARTITION BY HASH (user_id) PARTITIONS 10;\n\n-- Also acceptable\nPARTITION BY HASH (user_id) PARTITIONS 16;\n```\n\n### Mistake 3: Forgetting UPDATE GLOBAL INDEXES on Partition DDL\n\n```sql\n-- BAD: leaves global indexes UNUSABLE\nALTER TABLE ORDERS DROP PARTITION p_old;\n\n-- GOOD: maintains global index validity\nALTER TABLE ORDERS DROP PARTITION p_old UPDATE GLOBAL INDEXES;\n```\n\n### Mistake 4: Partitioning Small Tables\n\nPartitioning tables with fewer than a few million rows adds overhead (partition metadata, statistics gathering complexity) without benefit. Only partition tables where partition pruning will eliminate a meaningful portion of I/O.\n\n### Mistake 5: Not Indexing the Partition Key\n\nEven on a partitioned table, if queries filter by non-partition key columns, you still need indexes on those columns. Partition pruning only helps when the partition key is in the predicate.\n\n### Mistake 6: Choosing the Wrong Partition Key\n\nThe partition key must align with the most common query filter. A table partitioned by `LOAD_DATE` but queried by `TRANSACTION_DATE` will never prune. Analyze actual query patterns before choosing the partition key — this decision is difficult to reverse in production.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database 23ai VLDB and Partitioning Guide — Partition Concepts](https://docs.oracle.com/en/database/oracle/oracle-database/23/vldbg/partition-concepts.html)\n- [Oracle Database 23ai VLDB and Partitioning Guide — Partition Administration](https://docs.oracle.com/en/database/oracle/oracle-database/23/vldbg/)\n- [Oracle Database 23ai VLDB and Partitioning Guide — Partition Pruning](https://docs.oracle.com/en/database/oracle/oracle-database/21/vldbg/partition-pruning.html)\n- [Oracle Database 12c R2 — Automatic List Partitioning (oracle-base.com)](https://oracle-base.com/articles/12c/automatic-list-partitioning-12cr2)\n- [Oracle Database 11g R1 — Interval Partitioning (oracle-base.com)](https://oracle-base.com/articles/11g/partitioning-enhancements-11gr1)\n- [Oracle Database 12c R2 — Partitioning Enhancements (oracle-base.com)](https://oracle-base.com/articles/12c/partitioning-enhancements-12cr2)\n- [Oracle Database 12c R1 — Asynchronous Global Index Maintenance (oracle-base.com)](https://oracle-base.com/articles/12c/asynchronous-global-index-maintenance-for-drop-and-truncate-partition-12cr1)\n- [Oracle Database 23ai SQL Language Reference — ALTER TABLE](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/)\n- [Oracle Database Partitioning Option licensing](https://www.oracle.com/database/technologies/partitioning.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":23130,"content_sha256":"29ced897b7a90ebc03ca2f5dc010ff8db3f47998b1ca408d95645eff153d074d"},{"filename":"design/tablespace-design.md","content":"# Oracle Tablespace Design\n\n## Overview\n\nA **tablespace** is Oracle's logical storage container — a named group of one or more data files that provides a unit of allocation, administration, and backup. Every Oracle object (table, index, cluster, LOB) resides in a tablespace. Thoughtful tablespace design:\n\n- Isolates I/O across storage devices for different workload types\n- Simplifies backup and recovery (tablespace-level RMAN backups)\n- Controls space growth and allocation behavior\n- Enables independent transport and offline management of object groups\n- Prevents data files from interfering with each other during growth\n\nThis guide covers tablespace types, sizing strategies, the ASSM vs MSSM choice, and a recommended multi-tablespace layout for production Oracle databases.\n\n---\n\n## 1. Tablespace Types\n\n### Permanent Tablespaces\n\nStore persistent database objects: tables, indexes, views (their segments), clusters, LOBs. The majority of tablespace design focuses on permanent tablespaces.\n\n### Temporary Tablespaces\n\nStore session-level temporary segments: sort runs, hash join work areas, global temporary table data, temporary LOBs. Temporary tablespaces never need backup — they are always rebuilt on database creation or recreation.\n\n```sql\n-- Create a dedicated temporary tablespace\nCREATE TEMPORARY TABLESPACE temp_01\n TEMPFILE '/u02/oradata/ORCL/temp01.dbf' SIZE 2G AUTOEXTEND ON NEXT 512M MAXSIZE 20G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 1M;\n\n-- Assign as default temporary tablespace\nALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp_01;\n\n-- Temporary tablespace groups (multiple temp tablespaces per group for parallel query)\nCREATE TABLESPACE GROUP temp_group;\nALTER TABLESPACE temp_01 TABLESPACE GROUP temp_group;\nALTER TABLESPACE temp_02 TABLESPACE GROUP temp_group;\nALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp_group;\n```\n\n### Undo Tablespace\n\nStores undo segments used for transaction rollback, read consistency, and Oracle Flashback features. Managed automatically by Oracle (AUM — Automatic Undo Management) when `UNDO_MANAGEMENT = AUTO`.\n\n```sql\nCREATE UNDO TABLESPACE undo_01\n DATAFILE '/u02/oradata/ORCL/undo01.dbf' SIZE 4G AUTOEXTEND ON NEXT 1G MAXSIZE 50G;\n\nALTER SYSTEM SET UNDO_TABLESPACE = undo_01;\nALTER SYSTEM SET UNDO_RETENTION = 900; -- 15 minutes (in seconds)\n```\n\nUndo sizing rule of thumb:\n\n```\nUndo Size (bytes) = UNDO_RETENTION (seconds) x (DB block size x blocks changed per second)\n = UNDO_RETENTION x (SELECT value FROM v$parameter WHERE name='db_block_size')\n x (SELECT undoblks/((last_analyzed - first_analyzed)*86400) FROM v$undostat)\n```\n\n---\n\n## 2. Bigfile vs Smallfile Tablespaces\n\n### Smallfile Tablespaces (Classic, Default)\n\n- Up to **1022 data files** per tablespace\n- Each data file up to **4 million blocks** (32 GB with 8K blocks, 128 GB with 32K blocks)\n- Maximum tablespace size: **~32 PB** (1022 files x 32 TB each with 32K blocks)\n- File numbers are a limited database-wide resource (max 65534 files per database)\n- Finer-grained backup: back up individual files\n\n```sql\nCREATE TABLESPACE users_data\n DATAFILE '/u01/oradata/ORCL/users_data01.dbf' SIZE 2G\n AUTOEXTEND ON NEXT 256M MAXSIZE 32G,\n '/u01/oradata/ORCL/users_data02.dbf' SIZE 2G\n AUTOEXTEND ON NEXT 256M MAXSIZE 32G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE\n SEGMENT SPACE MANAGEMENT AUTO;\n```\n\n### Bigfile Tablespaces (10g+)\n\n- Exactly **1 data file** per tablespace\n- Data file up to **4 billion blocks** (32 TB with 8K blocks, up to 128 TB with 32K blocks)\n- Only 1 file number consumed per tablespace\n- Simpler management for very large tablespaces\n- RMAN backup of a bigfile tablespace may require altering default settings to ensure I/O parallelism\n- Required for Oracle Managed Files (OMF) on some storage configurations\n\n```sql\nCREATE BIGFILE TABLESPACE dw_facts_data\n DATAFILE '/u03/oradata/ORCL/dw_facts_data01.dbf' SIZE 100G\n AUTOEXTEND ON NEXT 10G MAXSIZE 10T\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE\n SEGMENT SPACE MANAGEMENT AUTO;\n\n-- Resize a bigfile tablespace (can resize the single file directly through tablespace)\nALTER TABLESPACE dw_facts_data RESIZE 200G; -- only valid for bigfile tablespaces\n```\n\n### Bigfile vs Smallfile Decision Guide\n\n| Factor | Smallfile | Bigfile |\n|---|---|---|\n| Number of files | Up to 1022 per TS | Exactly 1 per TS |\n| Individual file size | Up to ~128 TB | Up to ~128 TB |\n| File number consumption | Multiple files per TS | 1 file per TS |\n| Management simplicity | More files to track | Simpler (1 file) |\n| Best for | OLTP, medium-size tables, flexibility | Huge DW fact tables, ASM environments |\n| ASM compatibility | Both work | Preferred on ASM (ASM handles striping) |\n\n---\n\n## 3. Extent Management\n\nExtents are contiguous groups of Oracle blocks allocated to a segment. Extent management determines how Oracle tracks and allocates extents within a tablespace.\n\n### Locally Managed Tablespaces (LMT) — Always Use This\n\nOracle tracks extent allocation in a bitmap stored within the tablespace's data files themselves, not in the SYSTEM tablespace's data dictionary. This eliminates contention on the data dictionary and is the default for all tablespaces since Oracle 9i.\n\n```sql\n-- Uniform extent size: all extents the same size\nCREATE TABLESPACE dw_idx\n DATAFILE '/u02/oradata/ORCL/dw_idx01.dbf' SIZE 10G AUTOEXTEND ON NEXT 1G MAXSIZE 100G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 8M; -- every extent is exactly 8M\n\n-- Autoallocate: Oracle chooses extent sizes (64K -> 1M -> 8M -> 64M...)\nCREATE TABLESPACE users_data\n DATAFILE '/u01/oradata/ORCL/users_data01.dbf' SIZE 4G AUTOEXTEND ON NEXT 512M MAXSIZE 50G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE;\n```\n\nAutoallocate is likely sufficient for all workloads unless there are specific niche reasons to require a uniform extent size.\n\n**Uniform size guidelines:**\n\n| Object Type | Recommended Uniform Size |\n|---|---|\n| OLTP data | 1M or autoallocate |\n| OLTP indexes | 1M or autoallocate |\n| DW fact tables | 8M–32M |\n| DW dimension tables | 1M–4M |\n| LOB segments | 8M–64M |\n| Undo | 1M or autoallocate |\n| Temporary | 1M |\n\n### Dictionary Managed Tablespaces (DMT) — Legacy, Avoid\n\nOracle tracks extent allocation in the SYSTEM tablespace data dictionary tables. Causes severe contention on `UET db — Skillopedia and `FET db — Skillopedia tables. All new tablespaces should be locally managed.\n\n---\n\n## 4. Segment Space Management: ASSM vs MSSM\n\nSegment space management controls how Oracle tracks free space **within** individual database blocks in a segment (which blocks have room for new row inserts).\n\n### Automatic Segment Space Management (ASSM)\n\nUses a multi-level bitmap within the segment itself to track block free space. ASSM is the default since Oracle 9i and is almost always the correct choice.\n\n```sql\nCREATE TABLESPACE users_data\n DATAFILE '/u01/oradata/ORCL/users_data01.dbf' SIZE 4G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE\n SEGMENT SPACE MANAGEMENT AUTO; -- ASSM (default, recommended)\n```\n\n**ASSM advantages:**\n- Eliminates \"freelist\" contention on high-concurrency insert tables\n- `PCTUSED` parameter is ignored (Oracle manages it automatically)\n- Supports `SHRINK SPACE` and online segment reorganization\n- Required for Oracle In-Memory, Advanced Compression, and several other features\n\n### Manual Segment Space Management (MSSM)\n\nUses **freelists** — linked lists of blocks with available space — to track insertable blocks. Each segment has a configurable number of freelists (`FREELIST GROUPS`).\n\n```sql\nCREATE TABLESPACE legacy_data\n DATAFILE '/u01/oradata/ORCL/legacy01.dbf' SIZE 1G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE\n SEGMENT SPACE MANAGEMENT MANUAL; -- MSSM (legacy)\n\n-- With MSSM, PCTUSED and FREELISTS matter:\nCREATE TABLE LEGACY_TABLE (\n id NUMBER,\n data VARCHAR2(100)\n)\nTABLESPACE legacy_data\nPCTFREE 10 -- 10% reserved for updates\nPCTUSED 60 -- block re-added to freelist when used% drops below 60%\nSTORAGE (FREELISTS 4 FREELIST GROUPS 2); -- 4 freelists, 2 freelist groups (RAC)\n```\n\n**When MSSM might still appear:**\n- Very old databases migrated from Oracle 8i/9i\n- Specific third-party applications that require MSSM behavior\n- Temporary tablespaces (always use MSSM internally for sort segments)\n\n### ASSM vs MSSM Comparison\n\n| Feature | ASSM | MSSM |\n|---|---|---|\n| Concurrency | Excellent (bitmap, no contention) | Can contend on freelists |\n| PCTUSED | Ignored | Active parameter |\n| PCTFREE | Honored | Honored |\n| SHRINK SPACE | Supported | Not supported |\n| Advanced Compression | Supported | Not supported |\n| In-Memory | Supported | Not supported |\n| Block utilization visibility | `DBMS_SPACE.SPACE_USAGE` | `DBMS_SPACE.FREE_BLOCKS` |\n| Recommended | Yes (all new work) | No (legacy only) |\n\n---\n\n## 5. Recommended Multi-Tablespace Production Layout\n\nA well-designed production Oracle database separates objects by their I/O profile, growth rate, and recoverability requirements. The following layout covers OLTP and data warehouse workloads.\n\n### OLTP Production Layout\n\n```sql\n-- ============================================================\n-- SYSTEM and SYSAUX: managed by Oracle — never put user objects here\n-- ============================================================\n\n-- ============================================================\n-- UNDO: one per database instance (two for RAC: undo_01, undo_02)\n-- ============================================================\nCREATE UNDO TABLESPACE undo_01\n DATAFILE '/u02/oradata/ORCL/undo_01_01.dbf' SIZE 8G\n AUTOEXTEND ON NEXT 1G MAXSIZE 100G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE;\n\n-- ============================================================\n-- TEMP: one per database (or temp group for RAC/parallel query)\n-- ============================================================\nCREATE TEMPORARY TABLESPACE temp_01\n TEMPFILE '/u02/oradata/ORCL/temp_01_01.dbf' SIZE 4G\n AUTOEXTEND ON NEXT 512M MAXSIZE 50G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 1M;\n\nALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp_01;\n\n-- ============================================================\n-- APPLICATION DATA: one tablespace per schema or application module\n-- Split high-churn tables into separate tablespaces for targeted backup\n-- ============================================================\nCREATE TABLESPACE app_data\n DATAFILE '/u03/oradata/ORCL/app_data_01.dbf' SIZE 10G\n AUTOEXTEND ON NEXT 1G MAXSIZE 500G,\n '/u03/oradata/ORCL/app_data_02.dbf' SIZE 10G\n AUTOEXTEND ON NEXT 1G MAXSIZE 500G\n EXTENT MANAGEMENT LOCAL AUTOALLOCATE\n SEGMENT SPACE MANAGEMENT AUTO;\n\n--\n-- If you expect the need to do significant maintenance on LOBs, then\n-- optionally dedicate a separate tablespace for them. Usually not required.\n--\n-- Large object (LOB) tablespace — separate from row data\nCREATE TABLESPACE app_lob\n DATAFILE '/u04/oradata/ORCL/app_lob_01.dbf' SIZE 20G\n AUTOEXTEND ON NEXT 2G MAXSIZE 1T\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 8M -- uniform for LOB efficiency\n SEGMENT SPACE MANAGEMENT AUTO;\n\n-- ============================================================\n-- READ-ONLY: historical/archive data that never changes\n-- ============================================================\nCREATE TABLESPACE app_archive\n DATAFILE '/u06/oradata/ORCL/app_archive_01.dbf' SIZE 50G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 8M\n SEGMENT SPACE MANAGEMENT AUTO;\n-- After loading archive data:\nALTER TABLESPACE app_archive READ ONLY; -- prevents accidental modification + skips recovery\n```\n\n### Data Warehouse Layout\n\n```sql\n-- ============================================================\n-- DW DATA: separate tablespaces for facts vs dimensions\n-- ============================================================\nCREATE BIGFILE TABLESPACE dw_fact_data\n DATAFILE '/u07/oradata/ORCL/dw_fact_data_01.dbf' SIZE 200G\n AUTOEXTEND ON NEXT 20G MAXSIZE 10T\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 32M -- large uniform for large fact segments\n SEGMENT SPACE MANAGEMENT AUTO;\n\nCREATE TABLESPACE dw_dim_data\n DATAFILE '/u07/oradata/ORCL/dw_dim_data_01.dbf' SIZE 10G\n AUTOEXTEND ON NEXT 1G MAXSIZE 100G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 4M\n SEGMENT SPACE MANAGEMENT AUTO;\n\n-- ============================================================\n-- DW INDEXES: bitmap and B-tree indexes on DW tables\n-- ============================================================\nCREATE TABLESPACE dw_idx\n DATAFILE '/u08/oradata/ORCL/dw_idx_01.dbf' SIZE 20G\n AUTOEXTEND ON NEXT 2G MAXSIZE 500G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 8M\n SEGMENT SPACE MANAGEMENT AUTO;\n\n-- ============================================================\n-- STAGING: ETL staging area — NOT backed up (can be recreated)\n-- ============================================================\nCREATE TABLESPACE etl_stage\n DATAFILE '/u09/oradata/ORCL/etl_stage_01.dbf' SIZE 50G\n AUTOEXTEND ON NEXT 5G MAXSIZE 1T\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 16M\n SEGMENT SPACE MANAGEMENT AUTO;\n\n-- ============================================================\n-- DW TEMP: separate large temp tablespace for analytic queries\n-- ============================================================\nCREATE TEMPORARY TABLESPACE dw_temp\n TEMPFILE '/u09/oradata/ORCL/dw_temp_01.dbf' SIZE 50G\n AUTOEXTEND ON NEXT 5G MAXSIZE 500G\n EXTENT MANAGEMENT LOCAL UNIFORM SIZE 1M;\n\n-- Assign to DW schema users\nALTER USER dw_analyst_user TEMPORARY TABLESPACE dw_temp;\nALTER USER etl_process_user TEMPORARY TABLESPACE dw_temp;\n```\n\n---\n\n## 6. Tablespace Sizing\n\n### Initial Sizing Strategy\n\nNever create a tablespace at its maximum expected size upfront. Use `AUTOEXTEND` with a meaningful `MAXSIZE` cap:\n\n```sql\n-- Good pattern: small initial, controlled autoextend, firm maximum\nDATAFILE '/u01/oradata/ORCL/app_data_01.dbf'\n SIZE 1G -- initial size: allocate and format immediately\n AUTOEXTEND ON -- allow growth\n NEXT 512M -- grow by 512M at a time\n MAXSIZE 100G -- never exceed 100G per file\n```\n\n### Sizing Formulas\n\n**Table segment size estimate:**\n\n```sql\n-- Estimate segment size before creation\n-- DBMS_SPACE.CREATE_TABLE_COST is a PROCEDURE with OUT parameters, not a function.\n-- It cannot be called in a SELECT statement; use a PL/SQL block or bind variables.\nDECLARE\n v_used_bytes NUMBER;\n v_alloc_bytes NUMBER;\nBEGIN\n DBMS_SPACE.CREATE_TABLE_COST(\n tablespace_name => 'USERS_DATA',\n avg_row_size => 250, -- average bytes per row\n row_count => 10000000, -- expected row count\n pct_free => 10,\n used_bytes => v_used_bytes,\n alloc_bytes => v_alloc_bytes\n );\n DBMS_OUTPUT.PUT_LINE('Used bytes: ' || v_used_bytes);\n DBMS_OUTPUT.PUT_LINE('Alloc bytes: ' || v_alloc_bytes);\nEND;\n/\n```\n\n**Monitor existing tablespace usage:**\n\n```sql\nSELECT\n ts.tablespace_name,\n ts.block_size,\n ROUND(SUM(df.bytes) / 1073741824, 2) AS total_gb,\n ROUND(SUM(fs.free_space) / 1073741824, 2) AS free_gb,\n ROUND((1 - SUM(fs.free_space) / SUM(df.bytes)) * 100, 1) AS used_pct,\n ts.status,\n ts.contents,\n ts.extent_management,\n ts.segment_space_management\nFROM\n dba_tablespaces ts\n JOIN dba_data_files df ON ts.tablespace_name = df.tablespace_name\n LEFT JOIN (\n SELECT tablespace_name, SUM(bytes) AS free_space\n FROM dba_free_space\n GROUP BY tablespace_name\n ) fs ON ts.tablespace_name = fs.tablespace_name\nGROUP BY\n ts.tablespace_name, ts.block_size, ts.status, ts.contents,\n ts.extent_management, ts.segment_space_management\nORDER BY\n used_pct DESC NULLS LAST;\n```\n\n**Find top space consumers:**\n\n```sql\nSELECT\n owner,\n segment_name,\n segment_type,\n tablespace_name,\n ROUND(bytes / 1073741824, 3) AS size_gb\nFROM\n dba_segments\nWHERE\n tablespace_name NOT IN ('SYSTEM', 'SYSAUX')\nORDER BY\n bytes DESC\nFETCH FIRST 20 ROWS ONLY;\n```\n\n---\n\n## 7. Tablespace Maintenance\n\n### Adding Data Files\n\n```sql\n-- Add a new data file to an existing tablespace\nALTER TABLESPACE app_data ADD DATAFILE\n '/u03/oradata/ORCL/app_data_03.dbf' SIZE 10G\n AUTOEXTEND ON NEXT 1G MAXSIZE 500G;\n```\n\n### Resizing Data Files\n\n```sql\n-- Resize a specific data file\nALTER DATABASE DATAFILE '/u03/oradata/ORCL/app_data_01.dbf' RESIZE 20G;\n\n-- Resize with autoextend adjustment\nALTER DATABASE DATAFILE '/u03/oradata/ORCL/app_data_01.dbf'\n AUTOEXTEND ON NEXT 2G MAXSIZE 200G;\n```\n\n### Reclaiming Space — Segment Shrink\n\n```sql\n-- Enable row movement first (required for SHRINK)\nALTER TABLE APP_SCHEMA.LARGE_TABLE ENABLE ROW MOVEMENT;\n\n-- Compact the segment and reset HWM (two-phase)\nALTER TABLE APP_SCHEMA.LARGE_TABLE SHRINK SPACE COMPACT; -- phase 1: move rows online\nALTER TABLE APP_SCHEMA.LARGE_TABLE SHRINK SPACE; -- phase 2: reset HWM\n\n-- Or do both in one command (causes brief lock on HWM reset)\nALTER TABLE APP_SCHEMA.LARGE_TABLE SHRINK SPACE CASCADE; -- shrink table + all indexes\n\n-- After shrink, check for freed space\nSELECT segment_name, segment_type, ROUND(bytes/1048576, 2) AS size_mb\nFROM dba_segments\nWHERE segment_name = 'LARGE_TABLE';\n```\n\n### Moving a Table to a Different Tablespace\n\n```sql\n-- Online table move (12c+, requires ENABLE ROW MOVEMENT)\nALTER TABLE APP_SCHEMA.ORDERS\n MOVE TABLESPACE new_tablespace\n ONLINE;\n\n-- Offline move (classic — invalidates indexes, faster)\nALTER TABLE APP_SCHEMA.ORDERS MOVE TABLESPACE new_tablespace;\n\n-- Rebuild invalidated indexes after offline move\nSELECT 'ALTER INDEX ' || owner || '.' || index_name || ' REBUILD TABLESPACE app_idx;'\nFROM dba_indexes\nWHERE table_name = 'ORDERS'\nAND status = 'UNUSABLE';\n```\n\n---\n\n## 8. Default Tablespace Assignment\n\n```sql\n-- Set schema-level defaults for new objects\nALTER USER app_owner\n DEFAULT TABLESPACE app_data\n TEMPORARY TABLESPACE temp_01\n QUOTA UNLIMITED ON app_data\n QUOTA UNLIMITED ON app_idx\n QUOTA UNLIMITED ON app_lob;\n\n-- Quota management\nALTER USER app_owner QUOTA 50G ON app_data; -- restrict user to 50G\nALTER USER app_owner QUOTA 0 ON users; -- prevent use of USERS tablespace\n\n-- View user tablespace quotas\nSELECT tablespace_name, bytes_used, max_bytes\nFROM dba_ts_quotas\nWHERE username = 'APP_OWNER';\n```\n\n---\n\n## 9. Best Practices\n\n- **Separate OLTP data from DW data.** Different compression settings, extent sizes, and backup frequencies make separation critical.\n- **Never store application objects in SYSTEM or SYSAUX.** Corruption or space exhaustion in SYSTEM brings the entire database down.\n- **Cap AUTOEXTEND with a realistic MAXSIZE.** Unlimited autoextend on a filesystem will fill the disk and crash the database. Set `MAXSIZE` to leave at least 20% filesystem headroom.\n- **Put LOB segments in a dedicated tablespace.** LOB segments grow independently from their row tables; isolating them prevents space contention and simplifies monitoring.\n- **Create read-only tablespaces for historical archive data.** Read-only tablespaces don't need backup after the final datafile checkpoint, dramatically reducing backup windows.\n- **Monitor tablespace usage proactively.** Set Oracle thresholds or custom alerts at 75% and 85% usage — never wait for `ORA-01653: unable to extend table`.\n\n```sql\n-- Oracle Server-generated space alerts (built-in threshold management)\nEXEC DBMS_SERVER_ALERT.SET_THRESHOLD(\n metrics_id => DBMS_SERVER_ALERT.TABLESPACE_PCT_FULL,\n warning_operator => DBMS_SERVER_ALERT.OPERATOR_GE,\n warning_value => '75',\n critical_operator => DBMS_SERVER_ALERT.OPERATOR_GE,\n critical_value => '90',\n observation_period => 1,\n consecutive_occurrences => 1,\n instance_name => NULL,\n object_type => DBMS_SERVER_ALERT.OBJECT_TYPE_TABLESPACE,\n object_name => 'APP_DATA'\n);\n```\n\n---\n\n## 10. Common Mistakes and How to Avoid Them\n\n### Mistake 1: Storing User Objects in SYSTEM or USERS\n\nDefault tablespace for new schemas is `USERS` unless explicitly set. The `USERS` tablespace and especially `SYSTEM` should never hold application data.\n\n```sql\n-- Prevent this at user creation\nCREATE USER app_owner IDENTIFIED BY \"SecurePassword1!\"\n DEFAULT TABLESPACE app_data\n TEMPORARY TABLESPACE temp_01;\n-- Do NOT grant quota on SYSTEM or USERS\n```\n\n### Mistake 2: Unlimited AUTOEXTEND with No MAXSIZE\n\nA single runaway query generating excessive temp segment usage, or a bug causing an infinite insert loop, can exhaust the filesystem and crash the entire database instance.\n\n```sql\n-- BAD\nDATAFILE '/u01/oradata/ORCL/app_data_01.dbf' SIZE 1G AUTOEXTEND ON;\n\n-- GOOD\nDATAFILE '/u01/oradata/ORCL/app_data_01.dbf' SIZE 1G AUTOEXTEND ON NEXT 1G MAXSIZE 200G;\n```\n\n### Mistake 3: Using Dictionary Managed Tablespaces\n\nDMT causes serialization on `UET db — Skillopedia and `FET db — Skillopedia system tables. All new tablespaces must be locally managed. If you have old DMT tablespaces, migrate them:\n\n```sql\n-- Migrate dictionary-managed tablespace to locally managed\nEXEC DBMS_SPACE_ADMIN.TABLESPACE_MIGRATE_TO_LOCAL('OLD_DMT_TABLESPACE');\n```\n\n### Mistake 4: Using MSSM for New Tables on High-Concurrency Systems\n\nMSSM freelist contention is a documented scalability bottleneck. Always use ASSM (`SEGMENT SPACE MANAGEMENT AUTO`) for new tablespaces.\n\n### Mistake 5: Undersizing the Undo Tablespace\n\nAn undersized undo tablespace causes `ORA-01555: snapshot too old` errors for long-running queries and prevents Flashback Query from working for the intended retention window. Size undo based on the longest expected transaction and the `UNDO_RETENTION` setting.\n\n---\n\n## 11. Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features listed from 21c/23c generations are Oracle Database 26ai-capable; keep 19c alternatives for mixed-version estates.\n- Validate defaults and behavior in your exact RU level when running both 19c and 26ai.\n\n| Feature | Version |\n|---|---|\n| Locally Managed Tablespaces default | 9i |\n| ASSM default | 9i |\n| Bigfile Tablespaces | 10g |\n| Online Segment Shrink | 10g |\n| Undo Advisor (DBMS_UNDO_ADV) | 10g |\n| Tablespace Encryption (TDE) | 10g R2 |\n| Online Table Move | 12c R2 (12.2) |\n| Online Tablespace Migration to LMT | 10g |\n| Automatic Bigfile Tablespace default (Autonomous DB) | 19c (ADB) |\n| In-Memory tablespace (IM_IMCU_POOL) | 12c |\n\n---\n\n## Sources\n\n- [Oracle Database 23ai Administrator's Guide — Managing Tablespaces](https://docs.oracle.com/en/database/oracle/oracle-database/23/admin/managing-tablespaces.html)\n- [Oracle Database 23ai Concepts — Logical Storage Structures](https://docs.oracle.com/en/database/oracle/oracle-database/23/cncpt/logical-storage-structures.html)\n- [Oracle Database 19c Administrator's Guide — Managing Tablespaces](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-tablespaces.html)\n- [Oracle Database 23ai PL/SQL Packages and Types Reference — DBMS_SPACE](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/DBMS_SPACE.html)\n- [Oracle Database 19c PL/SQL Packages and Types Reference — DBMS_SERVER_ALERT](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_SERVER_ALERT.html)\n- [Oracle Bigfile Tablespace (oracle-faq.com)](https://www.orafaq.com/wiki/Bigfile_tablespace)\n- [Oracle Database 12c R2 — Online Table Move (oracle-base.com)](https://oracle-base.com/articles/12c/online-move-table-12cr2)\n- [Oracle ALTER TABLE SHRINK SPACE — Online Segment Shrink (oracle-base.com)](https://oracle-base.com/articles/misc/alter-table-shrink-space-online)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":23813,"content_sha256":"cdf66310b442d698472e76a2952200c885d9697db622c2838e623d0da1b89c54"},{"filename":"devops/database-testing.md","content":"# Database Testing with utPLSQL\n\n## Overview\n\nTesting PL/SQL code has historically been treated as a second-class concern — manual testing, ad hoc scripts, or no testing at all. The utPLSQL framework brings unit testing discipline to Oracle database development: structured test packages, assertions, setup/teardown lifecycle, mocking, code coverage measurement, and integration with CI/CD pipelines.\n\nutPLSQL (version 3.x) is the modern successor to utPLSQL v1 and Steven Feuerstein's original work. It runs entirely inside the Oracle database as a set of installed packages, requiring no external runtime. Tests are written in PL/SQL, making them native to the environment they test.\n\nThis guide covers the full lifecycle: writing tests, managing test data, mocking dependencies, integrating with pipelines, and measuring coverage.\n\n---\n\n## Installing utPLSQL\n\n```shell\n# Clone the repository\ngit clone https://github.com/utPLSQL/utPLSQL.git\ncd utPLSQL\n\n# Install into the database\n# The installer creates the UT3 schema and all framework objects\nsqlplus sys/password@//host:1521/service AS SYSDBA @install/install.sql\n\n# Create a dedicated tester account\nsqlplus sys/password@//host:1521/service AS SYSDBA \u003c\u003c'EOF'\nCREATE USER ut_runner IDENTIFIED BY \"password\";\nGRANT CREATE SESSION TO ut_runner;\nGRANT ut_runner TO ut_runner; -- utPLSQL role\nEOF\n```\n\n### Post-Installation Verification\n\n```sql\n-- Verify no invalid objects remain after install\nSELECT object_name, object_type, status\nFROM all_objects\nWHERE owner = 'UT3'\n AND object_type IN ('PACKAGE', 'TYPE')\n AND status = 'INVALID';\n-- Should return no rows\n\n-- Run the framework self-test\nBEGIN\n ut.run();\nEND;\n/\n```\n\n---\n\n## Test Package Structure\n\nutPLSQL tests are organized as annotated PL/SQL packages. Annotations (`-- %` comments) drive the framework:\n\n| Annotation | Purpose |\n|---|---|\n| `%suite` | Marks a package as a test suite |\n| `%suitepath(path)` | Hierarchical path for organizing suites (e.g., `myapp.orders`) |\n| `%test` | Marks a procedure as a test |\n| `%beforeall` | Runs once before the entire suite |\n| `%afterall` | Runs once after the entire suite |\n| `%beforeeach` | Runs before each test |\n| `%aftereach` | Runs after each test |\n| `%suite_context` / `%context` | Descriptive grouping label for sub-contexts |\n| `%endcontext` | Ends a context group |\n| `%displayname` | Human-readable test/suite name |\n| `%disabled` | Skip this test |\n| `%throws` | Expect a specific exception code |\n| `%tags(tag1,tag2)` | Tag for selective test running |\n\n### Minimal Test Package\n\n```sql\n-- Package specification\nCREATE OR REPLACE PACKAGE ut_pkg_orders AS\n -- %suite(Order Processing Tests)\n -- %suitepath(app.orders)\n\n -- %beforeall\n PROCEDURE setup_suite;\n\n -- %afterall\n PROCEDURE teardown_suite;\n\n -- %beforeeach\n PROCEDURE setup_test;\n\n -- %aftereach\n PROCEDURE teardown_test;\n\n -- %test(Calculate order total with multiple lines)\n PROCEDURE test_order_total_multiple_lines;\n\n -- %test(Calculate order total with single line)\n PROCEDURE test_order_total_single_line;\n\n -- %test(Raise exception for non-existent order)\n -- %throws(-20001)\n PROCEDURE test_order_total_invalid_id;\n\nEND ut_pkg_orders;\n/\n\n-- Package body\nCREATE OR REPLACE PACKAGE BODY ut_pkg_orders AS\n\n -- Suite-level constants\n c_test_customer_id CONSTANT NUMBER := -9001;\n c_test_order_id CONSTANT NUMBER := -9001;\n\n -- -----------------------------------------------------------------------\n -- Lifecycle hooks\n -- -----------------------------------------------------------------------\n\n PROCEDURE setup_suite IS\n BEGIN\n -- Insert a permanent test customer used by all tests\n INSERT INTO CUSTOMERS_T (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL)\n VALUES (c_test_customer_id, 'Test', 'User', '[email protected]');\n COMMIT;\n END setup_suite;\n\n PROCEDURE teardown_suite IS\n BEGIN\n -- Remove all test data by key range (negative IDs are test data)\n DELETE FROM ORDER_LINES WHERE ORDER_ID \u003c 0;\n DELETE FROM ORDERS WHERE ORDER_ID \u003c 0;\n DELETE FROM CUSTOMERS_T WHERE CUSTOMER_ID \u003c 0;\n COMMIT;\n END teardown_suite;\n\n PROCEDURE setup_test IS\n BEGIN\n -- Insert a fresh test order before each test\n INSERT INTO ORDERS (ORDER_ID, CUSTOMER_ID, ORDER_DATE, STATUS_CODE)\n VALUES (c_test_order_id, c_test_customer_id, SYSDATE, 'PENDING');\n COMMIT;\n END setup_test;\n\n PROCEDURE teardown_test IS\n BEGIN\n -- Clean up per-test data\n DELETE FROM ORDER_LINES WHERE ORDER_ID = c_test_order_id;\n DELETE FROM ORDERS WHERE ORDER_ID = c_test_order_id;\n COMMIT;\n END teardown_test;\n\n -- -----------------------------------------------------------------------\n -- Tests\n -- -----------------------------------------------------------------------\n\n PROCEDURE test_order_total_multiple_lines IS\n v_actual NUMBER;\n BEGIN\n -- Arrange: insert order lines\n INSERT INTO ORDER_LINES (LINE_ID, ORDER_ID, PRODUCT_ID, QTY, UNIT_PRICE)\n VALUES (-1, c_test_order_id, 1001, 2, 19.99);\n\n INSERT INTO ORDER_LINES (LINE_ID, ORDER_ID, PRODUCT_ID, QTY, UNIT_PRICE)\n VALUES (-2, c_test_order_id, 1002, 1, 49.99);\n\n COMMIT;\n\n -- Act\n v_actual := PKG_ORDERS.get_order_total(c_test_order_id);\n\n -- Assert\n ut.expect(v_actual).to_equal(89.97); -- 2*19.99 + 1*49.99\n END test_order_total_multiple_lines;\n\n PROCEDURE test_order_total_single_line IS\n v_actual NUMBER;\n BEGIN\n INSERT INTO ORDER_LINES (LINE_ID, ORDER_ID, PRODUCT_ID, QTY, UNIT_PRICE)\n VALUES (-1, c_test_order_id, 1001, 1, 100.00);\n COMMIT;\n\n v_actual := PKG_ORDERS.get_order_total(c_test_order_id);\n\n ut.expect(v_actual).to_equal(100.00);\n END test_order_total_single_line;\n\n PROCEDURE test_order_total_invalid_id IS\n v_dummy NUMBER;\n BEGIN\n -- Expect PKG_ORDERS to raise ORA-20001 for a non-existent order\n v_dummy := PKG_ORDERS.get_order_total(-99999);\n END test_order_total_invalid_id;\n\nEND ut_pkg_orders;\n/\n```\n\n---\n\n## Assertions\n\nutPLSQL uses a fluent assertion API centered on `ut.expect(actual).to_*(expected)`. All assertion methods support optional failure messages.\n\n```sql\n-- Scalar equality\nut.expect(v_count).to_equal(5);\nut.expect(v_count).not_to_equal(0);\nut.expect(v_name).to_equal('ACTIVE');\n\n-- Null checks\nut.expect(v_result).to_be_null();\nut.expect(v_result).not_to_be_null();\n\n-- Numeric comparisons\nut.expect(v_total).to_be_greater_than(0);\nut.expect(v_total).to_be_greater_or_equal(1);\nut.expect(v_total).to_be_less_than(1000);\nut.expect(v_total).to_be_less_or_equal(1000);\nut.expect(v_total).to_be_between(10, 999);\n\n-- Boolean\nut.expect(v_flag).to_be_true();\nut.expect(v_flag).to_be_false();\n\n-- Strings\nut.expect(v_message).to_be_like('%error%'); -- SQL LIKE pattern\nut.expect(v_message).not_to_be_like('%SUCCESS%');\nut.expect(v_name).to_be_like_ignoring_case('%smith%'); -- case-insensitive LIKE\nut.expect(v_message).to_match('^ERROR:.*\\d{4}

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

); -- REGEXP\n\n-- Custom failure message\nut.expect(v_status, 'Order status should be SHIPPED').to_equal('SHIPPED');\n```\n\n### Comparing Cursors\n\nCursor comparison is one of utPLSQL's most powerful features for testing queries:\n\n```sql\nPROCEDURE test_active_customer_view IS\nBEGIN\n ut.expect(\n CURSOR(\n SELECT CUSTOMER_ID, EMAIL, STATUS_CODE\n FROM CUSTOMERS\n WHERE CUSTOMER_ID = c_test_customer_id\n )\n ).to_equal(\n CURSOR(\n SELECT c_test_customer_id AS CUSTOMER_ID,\n '[email protected]' AS EMAIL,\n 'ACTIVE' AS STATUS_CODE\n FROM DUAL\n )\n );\nEND test_active_customer_view;\n```\n\nCursor comparisons support additional modifiers:\n\n```sql\n-- Exclude non-deterministic columns (e.g. timestamps) from comparison\nut.expect(l_actual).to_equal(l_expected)\n .exclude_columns(ut_varchar2_list('created_at', 'updated_at'));\n\n-- Unordered comparison (row order does not matter)\nut.expect(l_actual).to_equal(l_expected).unordered;\n```\n\n### Comparing Collections\n\n```sql\nPROCEDURE test_product_list IS\n TYPE t_ids IS TABLE OF NUMBER;\n v_actual t_ids;\n v_expected t_ids := t_ids(101, 102, 103);\nBEGIN\n SELECT PRODUCT_ID\n BULK COLLECT INTO v_actual\n FROM PRODUCTS\n WHERE CATEGORY = 'WIDGET'\n ORDER BY PRODUCT_ID;\n\n ut.expect(anydata.ConvertCollection(v_actual))\n .to_equal(anydata.ConvertCollection(v_expected));\nEND test_product_list;\n```\n\n### Exception Testing\n\n```sql\n-- Method 1: %throws annotation (cleanest for a single expected exception)\n-- %throws(-20001)\nPROCEDURE test_invalid_input IS\nBEGIN\n validate_customer(NULL); -- should raise ORA-20001\nEND;\n\n-- Method 2: Inline ut.expect().to_throw() for multiple exception cases\nPROCEDURE test_multiple_exception_cases IS\nBEGIN\n ut.expect(\n PROCEDURE() IS BEGIN validate_customer(NULL); END;\n ).to_throw(-20001);\n\n ut.expect(\n PROCEDURE() IS BEGIN validate_customer(-1); END;\n ).to_throw(-20002, 'negative customer id');\nEND;\n```\n\n---\n\n## Test Data Management\n\n### Strategies for Isolating Test Data\n\n**Negative ID Convention:** Reserve negative or very high integer ranges for test data. Teardown deletes by this range. Simple, effective, and prevents accidental deletion of real data.\n\n```sql\n-- Test data IDs: -9999 to -1\n-- Production data IDs: 1+\nc_test_id_offset CONSTANT NUMBER := -9000;\n```\n\n**Savepoint-Based Rollback:** Roll back after each test, avoiding permanent inserts. Fastest approach, but cannot test COMMIT-dependent logic.\n\n```sql\nPROCEDURE setup_test IS\nBEGIN\n SAVEPOINT test_start;\nEND setup_test;\n\nPROCEDURE teardown_test IS\nBEGIN\n ROLLBACK TO SAVEPOINT test_start;\nEND teardown_test;\n```\n\n**Autonomous Transaction for Persistent Test Data:** Insert reference/setup data that must survive the test transaction context (e.g., lookup tables required by the code under test):\n\n```sql\n-- %beforeall\nPROCEDURE setup_test_data IS\n PRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\n INSERT INTO test_customers VALUES (99999, 'TEST CORP', 'ACTIVE');\n COMMIT;\nEND setup_test_data;\n\n-- %afterall\nPROCEDURE cleanup_test_data IS\n PRAGMA AUTONOMOUS_TRANSACTION;\nBEGIN\n DELETE FROM test_customers WHERE customer_id = 99999;\n COMMIT;\nEND cleanup_test_data;\n```\n\n**Dedicated Test Schema:** Run tests in a completely separate schema that is dropped and recreated per CI run. Best isolation, highest overhead.\n\n```sql\n-- CI pipeline creates a test schema:\nCREATE USER test_schema IDENTIFIED BY \"password\";\nGRANT CREATE SESSION, CREATE TABLE, CREATE PROCEDURE TO test_schema;\n\n-- Apply migrations to test_schema\n-- Run tests\n-- Drop test_schema after run\n```\n\n### Test Data Builders\n\nA PL/SQL builder pattern avoids scattering INSERT statements across test packages:\n\n```sql\nCREATE OR REPLACE PACKAGE TEST_DATA_BUILDER AS\n\n FUNCTION build_customer(\n p_customer_id NUMBER DEFAULT -9001,\n p_first_name VARCHAR2 DEFAULT 'Test',\n p_last_name VARCHAR2 DEFAULT 'User',\n p_email VARCHAR2 DEFAULT '[email protected]',\n p_status VARCHAR2 DEFAULT 'ACTIVE'\n ) RETURN NUMBER; -- Returns the customer_id\n\n FUNCTION build_order(\n p_order_id NUMBER DEFAULT -9001,\n p_customer_id NUMBER DEFAULT -9001,\n p_status VARCHAR2 DEFAULT 'PENDING'\n ) RETURN NUMBER; -- Returns the order_id\n\nEND TEST_DATA_BUILDER;\n/\n\nCREATE OR REPLACE PACKAGE BODY TEST_DATA_BUILDER AS\n\n FUNCTION build_customer(\n p_customer_id NUMBER DEFAULT -9001,\n p_first_name VARCHAR2 DEFAULT 'Test',\n p_last_name VARCHAR2 DEFAULT 'User',\n p_email VARCHAR2 DEFAULT '[email protected]',\n p_status VARCHAR2 DEFAULT 'ACTIVE'\n ) RETURN NUMBER IS\n BEGIN\n INSERT INTO CUSTOMERS_T (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, STATUS_CODE)\n VALUES (p_customer_id, p_first_name, p_last_name, p_email, p_status);\n RETURN p_customer_id;\n END build_customer;\n\n FUNCTION build_order(\n p_order_id NUMBER DEFAULT -9001,\n p_customer_id NUMBER DEFAULT -9001,\n p_status VARCHAR2 DEFAULT 'PENDING'\n ) RETURN NUMBER IS\n BEGIN\n INSERT INTO ORDERS (ORDER_ID, CUSTOMER_ID, ORDER_DATE, STATUS_CODE)\n VALUES (p_order_id, p_customer_id, SYSDATE, p_status);\n RETURN p_order_id;\n END build_order;\n\nEND TEST_DATA_BUILDER;\n/\n```\n\n---\n\n## Mocking with utPLSQL\n\nutPLSQL includes a mocking framework that stubs out package function/procedure calls. This allows testing a package in isolation from its dependencies (external services, other packages, expensive queries).\n\n```sql\n-- Suppose PKG_PRICING calls an external rate service via PKG_EXCHANGE_RATES\n-- We want to test PKG_PRICING without the real exchange rate lookup\n\nPROCEDURE test_price_in_eur IS\nBEGIN\n -- Mock PKG_EXCHANGE_RATES.get_rate to always return 0.92\n ut3.ut_mock.package_function(\n a_owner => 'APP_OWNER',\n a_package => 'PKG_EXCHANGE_RATES',\n a_name => 'GET_RATE',\n a_return_value => 0.92\n );\n\n -- Now call the function under test\n ut.expect(\n PKG_PRICING.convert_to_eur(p_amount_usd => 100.00)\n ).to_equal(92.00);\n\n -- Verify the mock was called with expected arguments\n ut3.ut_mock.expect_called(\n a_owner => 'APP_OWNER',\n a_package => 'PKG_EXCHANGE_RATES',\n a_name => 'GET_RATE',\n a_times => 1\n );\nEND test_price_in_eur;\n```\n\n### Mocking Sequence-Based ID Generation\n\n```sql\n-- Mock a sequence by stubbing the function that wraps NEXTVAL\nut3.ut_mock.package_function(\n a_owner => 'APP_OWNER',\n a_package => 'PKG_ID_GENERATOR',\n a_name => 'NEXT_ORDER_ID',\n a_return_value => 42\n);\n```\n\n---\n\n## TDD Workflow for PL/SQL\n\n### Red-Green-Refactor Cycle\n\n1. **Write a failing test** that describes the desired behavior.\n2. **Write the minimum PL/SQL** to make the test pass.\n3. **Refactor** the implementation while keeping tests green.\n4. **Repeat** for the next behavior.\n\n```sql\n-- Step 1: Write the failing test first\n-- (The procedure PROCESS_REFUND does not exist yet)\n\nCREATE OR REPLACE PACKAGE ut_pkg_refunds AS\n -- %suite(Refund Processing)\n -- %test(Successful refund creates credit memo)\n PROCEDURE test_refund_creates_credit_memo;\nEND ut_pkg_refunds;\n/\n\nCREATE OR REPLACE PACKAGE BODY ut_pkg_refunds AS\n PROCEDURE test_refund_creates_credit_memo IS\n v_memo_id NUMBER;\n v_order_id NUMBER := -9001;\n BEGIN\n -- Arrange\n INSERT INTO ORDERS (ORDER_ID, CUSTOMER_ID, ORDER_DATE, STATUS_CODE, TOTAL_AMOUNT)\n VALUES (v_order_id, -9001, SYSDATE - 5, 'SHIPPED', 150.00);\n COMMIT;\n\n -- Act: this call fails until we implement the procedure\n PKG_REFUNDS.process_refund(\n p_order_id => v_order_id,\n p_amount => 50.00,\n o_memo_id => v_memo_id\n );\n\n -- Assert\n ut.expect(v_memo_id).not_to_be_null();\n\n DECLARE\n v_status VARCHAR2(20);\n BEGIN\n SELECT STATUS INTO v_status FROM CREDIT_MEMOS WHERE MEMO_ID = v_memo_id;\n ut.expect(v_status).to_equal('PENDING');\n END;\n\n ROLLBACK;\n END test_refund_creates_credit_memo;\nEND ut_pkg_refunds;\n/\n```\n\n```shell\n# Run the test and watch it fail (RED)\nsql /nolog \u003c\u003c'EOF'\n connect ut_runner/password@//host:1521/service\n SET SERVEROUTPUT ON\n EXEC ut.run('ut_pkg_refunds');\nEOF\n```\n\nNow implement the procedure, re-run, and watch it pass (GREEN).\n\n---\n\n## Running Tests\n\n```sql\n-- Run all tests in the database\nEXEC ut.run();\n\n-- Run a specific suite\nEXEC ut.run('ut_pkg_orders');\n\n-- Run a specific test by path\nEXEC ut.run('ut_pkg_orders.test_order_total_multiple_lines');\n\n-- Run tests matching a tag\nEXEC ut.run(a_tags => ut_varchar2_list('order', 'critical'));\n\n-- Run with specific reporter\nBEGIN\n ut.run(\n a_paths => ut_varchar2_list('app.orders'),\n a_reporter => ut_documentation_reporter()\n );\nEND;\n/\n```\n\n---\n\n## Integrating DB Tests into CI/CD\n\n### Output Reporters\n\nutPLSQL supports multiple output formats for CI integration:\n\n| Reporter | Output Format | Use Case |\n|---|---|---|\n| `ut_documentation_reporter` | Human-readable text | Local development |\n| `ut_junit_reporter` | JUnit XML | CI/CD (Jenkins, GitLab CI, GitHub Actions) |\n| `ut_sonar_test_reporter` | SonarQube format | SonarQube integration |\n| `ut_teamcity_reporter` | TeamCity format | JetBrains TeamCity |\n| `ut_tap_reporter` | TAP (Test Anything Protocol) | Generic CI tools |\n| `ut_coveralls_reporter` | Coveralls JSON | Coveralls code coverage service |\n| `ut_coverage_html_reporter` | HTML | Human-readable coverage report |\n| `ut_coverage_cobertura_reporter` | Cobertura XML | Coverage in CI dashboards |\n\n```sql\n-- JUnit XML (consumed by Jenkins, GitHub Actions, GitLab CI)\nBEGIN\n ut.run(\n a_paths => ut_varchar2_list(':app'),\n a_reporters => ut_reporters(\n ut_junit_reporter()\n ),\n a_output_to => ut_output_to_file('/tmp/test-results.xml')\n );\nEND;\n/\n```\n\n```sql\n-- Teamcity format\nBEGIN\n ut.run(\n a_reporters => ut_reporters(ut_teamcity_reporter())\n );\nEND;\n/\n\n-- Sonar Qube compatible\nBEGIN\n ut.run(\n a_reporters => ut_reporters(ut_sonar_test_reporter())\n );\nEND;\n/\n```\n\n### CI Pipeline Integration\n\n```yaml\n# .github/workflows/db-test.yml\nname: Database Tests\n\non:\n pull_request:\n paths:\n - 'db/**'\n - 'src/plsql/**'\n\njobs:\n test:\n runs-on: ubuntu-latest\n services:\n oracle:\n image: gvenzl/oracle-free:23-slim\n env:\n ORACLE_PASSWORD: testpassword\n ports:\n - 1521:1521\n options: >-\n --health-cmd \"sqlplus -L sys/testpassword@//localhost:1521/FREEPDB1 AS SYSDBA \u003c /dev/null\"\n --health-interval 30s\n --health-timeout 10s\n --health-retries 10\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Install utPLSQL\n run: |\n sqlplus sys/testpassword@//localhost:1521/FREEPDB1 AS SYSDBA \\\n @utPLSQL/install/install.sql\n\n - name: Apply schema migrations\n run: |\n liquibase \\\n --url=\"jdbc:oracle:thin:@//localhost:1521/FREEPDB1\" \\\n --username=sys --password=testpassword \\\n --defaultSchemaName=APP_OWNER \\\n update\n\n - name: Deploy test packages\n run: |\n sqlplus app_owner/password@//localhost:1521/FREEPDB1 \\\n @tests/install_tests.sql\n\n - name: Run utPLSQL tests\n run: |\n sqlplus ut_runner/password@//localhost:1521/FREEPDB1 \u003c\u003c'EOF'\n WHENEVER SQLERROR EXIT FAILURE\n BEGIN\n ut.run(\n a_reporters => ut_reporters(\n ut_junit_reporter(),\n ut_documentation_reporter()\n ),\n a_output_to => ut_output_to_file('/tmp/test-results.xml')\n );\n END;\n /\n EXIT\n EOF\n\n - name: Upload test results\n uses: actions/upload-artifact@v4\n if: always()\n with:\n name: test-results\n path: /tmp/test-results.xml\n\n - name: Publish test results\n uses: EnricoMi/publish-unit-test-result-action@v2\n if: always()\n with:\n files: /tmp/test-results.xml\n```\n\n### utPLSQL CLI (Java Client)\n\nThe utPLSQL-cli Java client provides a convenient command-line interface that pulls results from the database without requiring SQL*Plus:\n\n```shell\n# Install\nwget https://github.com/utPLSQL/utPLSQL-cli/releases/latest/download/utplsql-cli.zip\nunzip utplsql-cli.zip\n\n# Run tests with JUnit output\n./utplsql run app_owner/password@//host:1521/service \\\n -f=ut_junit_reporter -o=test-results.xml \\\n -f=ut_documentation_reporter -o=/dev/stdout \\\n -source_path=src/plsql \\\n -test_path=tests\n```\n\n---\n\n## Code Coverage\n\nutPLSQL integrates with Oracle's built-in DBMS_PROFILER and DBMS_PLSQL_CODE_COVERAGE to measure which lines of PL/SQL are executed during test runs.\n\n```sql\n-- Run tests with coverage collection\nBEGIN\n ut.run(\n a_paths => ut_varchar2_list(':app'),\n a_reporters => ut_reporters(\n ut_documentation_reporter(),\n ut_coverage_html_reporter()\n ),\n a_coverage_schemes => ut_varchar2_list('APP_OWNER'),\n a_output_to => ut_output_to_file('/tmp/coverage.html')\n );\nEND;\n/\n```\n\n```shell\n# utPLSQL-cli with coverage\n./utplsql run app_owner/password@//host:1521/service \\\n -f=ut_documentation_reporter -o=/dev/stdout \\\n -f=ut_coverage_html_reporter -o=coverage.html \\\n -f=ut_coverage_cobertura_reporter -o=coverage.xml \\\n -source_path=src/plsql \\\n -test_path=tests \\\n -coverage_schemes=APP_OWNER\n```\n\n### Checking Coverage Programmatically\n\n```sql\n-- Query coverage results after a test run (UT3.UT_COVERAGE_DETAILS view)\nSELECT\n o.OWNER,\n o.OBJECT_NAME,\n o.OBJECT_TYPE,\n c.COVERED_LINES,\n c.UNCOVERED_LINES,\n c.TOTAL_LINES,\n ROUND(c.COVERED_LINES / NULLIF(c.TOTAL_LINES, 0) * 100, 1) AS PCT_COVERAGE\nFROM\n UT3.UT_COVERAGE_DETAILS c\nJOIN\n ALL_OBJECTS o ON o.OBJECT_ID = c.OBJECT_ID\nWHERE\n o.OWNER = 'APP_OWNER'\nORDER BY\n PCT_COVERAGE ASC;\n\n-- Alternative view: UT3.UT_COVERAGE_RESULTS\nSELECT object_name, object_type,\n covered_lines,\n total_lines,\n ROUND(covered_lines / NULLIF(total_lines, 0) * 100, 1) AS pct_covered\nFROM ut3.ut_coverage_results\nORDER BY pct_covered;\n```\n\n---\n\n## Best Practices\n\n- **Test behavior, not implementation.** Assert on observable outcomes (return values, rows inserted, exceptions raised) rather than internal state or intermediate variables. Tests should survive refactoring without changes.\n- **Keep tests fast.** Each test should complete in milliseconds. Tests that run slow reduce developer feedback loops and tempt teams to skip running them. Avoid full-table queries in test setup; use targeted inserts with known primary keys.\n- **Use negative IDs for all test data.** This isolates test data from application data in shared databases and makes cleanup deterministic: `DELETE WHERE ID \u003c 0`.\n- **Test exception paths explicitly.** The `%throws` annotation and `ut.expect(...)throws(...)` pattern make error path testing clean. PL/SQL exception handling is a common source of bugs that only surface in edge cases.\n- **Co-locate test packages with the code they test.** A `src/plsql/pkg_orders.pks` should have a corresponding `tests/ut_pkg_orders.pks` in the same repository.\n- **Run tests on every pull request.** Use the JUnit XML reporter to integrate with the PR check system. A red test should block merge.\n- **Measure and gate on coverage.** Aim for 80%+ line coverage on business-critical packages. Use coverage reports in CI to identify gaps, but do not treat 100% as the goal — test quality matters more than test quantity.\n- **Use `%tags` to separate fast unit tests from slow integration tests.** This allows running the fast suite on every commit and the full suite nightly or on release branches.\n- **Use cursor assertions over COUNT(*) checks.** Cursor comparison validates actual data values; a COUNT of 1 only tells you a row exists.\n\n---\n\n## Common Mistakes\n\n**Mistake: Tests that depend on execution order.**\nEach test should be independently runnable. Tests that rely on data left behind by a previous test are fragile and misleading. Use `%beforeeach` to set up fresh state before every test.\n\n**Mistake: Committing test data without cleanup.**\nIf `teardown` procedures fail (e.g., due to constraint violations in cleanup order), test data accumulates across runs. Use the negative ID convention and clean up in the correct dependency order: lines before headers, child records before parents.\n\n**Mistake: Testing the framework, not the code.**\nWriting tests that simply call `ut.expect(1).to_equal(1)` or test Oracle built-in behavior adds noise without value. Every test should exercise a specific behavior of application code.\n\n**Mistake: Ignoring recompilation errors in test packages.**\nIf the code under test changes its interface (renamed parameter, changed type), test packages compile with errors but utPLSQL may report them as failures rather than compilation problems. Check `USER_ERRORS` after deploying both application code and test packages.\n\n**Mistake: Running tests against a production database.**\nutPLSQL tests insert, update, and delete data. They should never run against production — not even with careful cleanup. Use dedicated test environments or ephemeral CI containers.\n\n**Mistake: Using `WHEN OTHERS THEN NULL` in test procedures.**\nSwallowing exceptions in test code causes tests to pass even when the code under test throws. Never suppress exceptions inside test procedures.\n\n**Mistake: Asserting only COUNT(*).**\nA count of 1 only confirms a row exists; it does not validate its content. Use cursor assertions to verify actual column values.\n\n**Mistake: Hardcoded IDs that conflict with existing data.**\nUse sequences, known-safe negative ranges, or very large integers (e.g., 99999+) to avoid collisions with application data.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [utPLSQL Documentation](https://utplsql.org/utPLSQL/latest/) — test package annotations, assertion API, reporters, coverage\n- [utPLSQL GitHub Repository](https://github.com/utPLSQL/utPLSQL) — installation, version 3.x architecture\n- [utPLSQL-cli GitHub Repository](https://github.com/utPLSQL/utPLSQL-cli) — Java CLI client for test execution\n- [DBMS_PLSQL_CODE_COVERAGE (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_PLSQL_CODE_COVERAGE.html) — code coverage infrastructure used by utPLSQL\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":25757,"content_sha256":"268988c0c76d3c9984caa303efe37bde812ee762cd92851655b6c713ddb638f8"},{"filename":"devops/edition-based-redefinition.md","content":"# Edition-Based Redefinition (EBR) in Oracle DB\n\n## Overview\n\nEdition-Based Redefinition (EBR) is Oracle's mechanism for deploying application changes to a live database without any downtime — including changes to PL/SQL code, views, and synonyms. It allows multiple versions of these objects to coexist simultaneously in the database, each within its own named **edition**. Database sessions are associated with a specific edition, so old application instances and new application instances can run concurrently against the same database, each seeing their own version of the code.\n\nEBR was introduced in Oracle 11g Release 2 and is the canonical Oracle approach to hot-rollover (blue/green or rolling) deployments at the database tier. It is significantly more capable than simply replacing packages — it handles the entire application schema version lifecycle including backward-compatible view evolution and cross-edition data synchronization.\n\n---\n\n## Core Concepts\n\n### Editions\n\nAn edition is a named, schema-independent container for editionable objects. Editions form a tree rooted at the default edition (`ORA$BASE`). Child editions inherit all objects from their parent edition; changes made in a child edition override the parent's version for sessions running in that edition.\n\n```\nORA$BASE (root edition)\n └── V1 (initial production)\n └── V2 (in-flight deployment)\n └── V3 (next deployment)\n```\n\n```sql\n-- List all editions in the database\nSELECT EDITION_NAME, PARENT_EDITION_NAME, USABLE\nFROM DBA_EDITIONS\nORDER BY EDITION_NAME;\n\n-- Current edition of the session\nSELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') FROM DUAL;\n\n-- Default edition for the database\nSELECT PROPERTY_VALUE FROM DATABASE_PROPERTIES\nWHERE PROPERTY_NAME = 'DEFAULT_EDITION';\n```\n\n### Editionable Object Types\n\nNot all database objects are editionable. Only the following object types can have edition-specific versions:\n\n| Editionable | Not Editionable |\n|---|---|\n| PROCEDURE | TABLE |\n| FUNCTION | INDEX |\n| PACKAGE (spec + body) | SEQUENCE |\n| TRIGGER | MATERIALIZED VIEW |\n| TYPE (spec + body) | GRANT |\n| VIEW | DATABASE LINK |\n| SYNONYM | |\n| LIBRARY | |\n| SQL Translation Profile | |\n\nTables, indexes, and sequences are shared across all editions. This is fundamental to the design: EBR manages code versioning, not data versioning.\n\n### Enabling Editions on a Schema\n\n```sql\n-- Editions must be enabled for each schema that will use EBR\n-- Requires ALTER USER privilege\nALTER USER app_owner ENABLE EDITIONS;\n\n-- Verify\nSELECT USERNAME, EDITIONS_ENABLED\nFROM DBA_USERS\nWHERE USERNAME = 'APP_OWNER';\n```\n\n### Creating and Using Editions\n\n```sql\n-- Create a new edition (requires CREATE EDITION system privilege)\nCREATE EDITION v2 AS CHILD OF v1;\n\n-- Set the database default edition (new sessions use this edition)\nALTER DATABASE DEFAULT EDITION = v2;\n\n-- Set the edition for a specific session\nALTER SESSION SET EDITION = v2;\n\n-- Grant USE on an edition to a user\nGRANT USE ON EDITION v2 TO app_user;\n```\n\n---\n\n## Editioning Views\n\nBecause tables are not editionable, EBR introduces the **editioning view** as the boundary between code (editionable) and data (non-editionable). Application code never queries a base table directly; it queries an editioning view. During a deployment, the editioning view can be redefined in the new edition to expose a different column layout while the base table contains data for both the old and new schema.\n\n### Creating an Editioning View\n\n```sql\n-- The base table contains all columns for current and transitional state\nCREATE TABLE CUSTOMERS_T (\n CUSTOMER_ID NUMBER(18,0) NOT NULL,\n -- Old columns (present in V1)\n FULL_NAME VARCHAR2(200),\n -- New columns (added for V2 deployment)\n FIRST_NAME VARCHAR2(100),\n LAST_NAME VARCHAR2(100),\n EMAIL VARCHAR2(320) NOT NULL,\n STATUS_CODE VARCHAR2(10) DEFAULT 'ACTIVE' NOT NULL,\n CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n CONSTRAINT PK_CUSTOMERS_T PRIMARY KEY (CUSTOMER_ID)\n);\n\n-- V1 editioning view: exposes the old column layout\n-- (Run while connected to V1 edition)\nCREATE OR REPLACE EDITIONING VIEW CUSTOMERS AS\nSELECT\n CUSTOMER_ID,\n FULL_NAME,\n EMAIL,\n STATUS_CODE,\n CREATED_AT\nFROM CUSTOMERS_T;\n```\n\n```sql\n-- V2 editioning view: exposes the new column layout\n-- (Run while connected to V2 edition)\nALTER SESSION SET EDITION = v2;\n\nCREATE OR REPLACE EDITIONING VIEW CUSTOMERS AS\nSELECT\n CUSTOMER_ID,\n FIRST_NAME,\n LAST_NAME,\n EMAIL,\n STATUS_CODE,\n CREATED_AT\nFROM CUSTOMERS_T;\n```\n\nSessions in edition `v1` see the `FULL_NAME` layout. Sessions in edition `v2` see the `FIRST_NAME`, `LAST_NAME` layout. Both query the same physical `CUSTOMERS_T` table.\n\n---\n\n## Crossedition Triggers\n\nSince both editions write to the same base table, a mechanism is needed to keep data consistent across the column layouts. **Crossedition triggers** propagate writes from one edition's column layout to the other.\n\n### Forward Crossedition Trigger\n\nA forward crossedition trigger fires on the old edition and propagates changes to the new columns, so that data written by old-edition sessions is visible to new-edition sessions.\n\n```sql\n-- Connect as V1 edition, create the forward trigger\nALTER SESSION SET EDITION = v1;\n\nCREATE OR REPLACE TRIGGER TRG_CUST_FORWARD\nBEFORE INSERT OR UPDATE ON CUSTOMERS_T\nFOR EACH ROW\nFORWARD CROSSEDITION\nDISABLE\nBEGIN\n -- When old code writes FULL_NAME, split it into FIRST_NAME / LAST_NAME\n IF :NEW.FULL_NAME IS NOT NULL THEN\n :NEW.FIRST_NAME := REGEXP_SUBSTR(:NEW.FULL_NAME, '^\\S+');\n :NEW.LAST_NAME := REGEXP_SUBSTR(:NEW.FULL_NAME, '\\S+

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

);\n END IF;\nEND;\n/\n\n-- Enable the trigger once the V2 deployment is ready to start accepting traffic\nALTER TRIGGER TRG_CUST_FORWARD ENABLE;\n```\n\n### Reverse Crossedition Trigger\n\nA reverse crossedition trigger fires on the new edition and propagates changes back to the old columns, keeping old-edition sessions consistent while they are still running.\n\n```sql\n-- Connect as V2 edition, create the reverse trigger\nALTER SESSION SET EDITION = v2;\n\nCREATE OR REPLACE TRIGGER TRG_CUST_REVERSE\nBEFORE INSERT OR UPDATE ON CUSTOMERS_T\nFOR EACH ROW\nREVERSE CROSSEDITION\nDISABLE\nBEGIN\n -- When new code writes FIRST_NAME and LAST_NAME, reconstruct FULL_NAME\n IF :NEW.FIRST_NAME IS NOT NULL OR :NEW.LAST_NAME IS NOT NULL THEN\n :NEW.FULL_NAME := TRIM(:NEW.FIRST_NAME || ' ' || :NEW.LAST_NAME);\n END IF;\nEND;\n/\n\nALTER TRIGGER TRG_CUST_REVERSE ENABLE;\n```\n\n---\n\n## Hot-Rollover Deployment Workflow\n\nThe full deployment process for a hot-rollover using EBR follows a well-defined sequence:\n\n### Phase 1: Prepare the New Edition\n\n```sql\n-- 1. Create the new edition\nCREATE EDITION v2 AS CHILD OF v1;\n\n-- 2. Add new columns to the base table (additive, non-breaking)\nALTER TABLE CUSTOMERS_T ADD (\n FIRST_NAME VARCHAR2(100),\n LAST_NAME VARCHAR2(100)\n);\n\n-- 3. Switch to new edition and deploy new code\nALTER SESSION SET EDITION = v2;\n\n-- 4. Create the new editioning view (V2 layout)\nCREATE OR REPLACE EDITIONING VIEW CUSTOMERS AS\nSELECT CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL, STATUS_CODE, CREATED_AT\nFROM CUSTOMERS_T;\n\n-- 5. Deploy updated PL/SQL packages in V2 edition\nCREATE OR REPLACE PACKAGE PKG_CUSTOMERS AS\n PROCEDURE create_customer(\n p_first_name IN VARCHAR2,\n p_last_name IN VARCHAR2,\n p_email IN VARCHAR2\n );\nEND PKG_CUSTOMERS;\n/\n\nCREATE OR REPLACE PACKAGE BODY PKG_CUSTOMERS AS\n PROCEDURE create_customer(\n p_first_name IN VARCHAR2,\n p_last_name IN VARCHAR2,\n p_email IN VARCHAR2\n ) IS\n BEGIN\n INSERT INTO CUSTOMERS (CUSTOMER_ID, FIRST_NAME, LAST_NAME, EMAIL)\n VALUES (SEQ_CUSTOMER_ID.NEXTVAL, p_first_name, p_last_name, p_email);\n COMMIT;\n END create_customer;\nEND PKG_CUSTOMERS;\n/\n```\n\n### Phase 2: Enable Crossedition Triggers\n\n```sql\n-- Enable forward crossedition trigger (in V1) to propagate old writes to new columns\nALTER SESSION SET EDITION = v1;\nALTER TRIGGER TRG_CUST_FORWARD ENABLE;\n\n-- Enable reverse crossedition trigger (in V2) to propagate new writes to old columns\nALTER SESSION SET EDITION = v2;\nALTER TRIGGER TRG_CUST_REVERSE ENABLE;\n```\n\n### Phase 3: Backfill Existing Data\n\n```sql\n-- Populate new columns for rows that were inserted before the triggers were enabled\nALTER SESSION SET EDITION = v2;\n\nUPDATE CUSTOMERS_T\nSET\n FIRST_NAME = REGEXP_SUBSTR(FULL_NAME, '^\\S+'),\n LAST_NAME = REGEXP_SUBSTR(FULL_NAME, '\\S+

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

)\nWHERE\n FULL_NAME IS NOT NULL\n AND (FIRST_NAME IS NULL OR LAST_NAME IS NULL);\n\nCOMMIT;\n```\n\n### Phase 4: Switch Traffic to New Edition\n\n```sql\n-- Set V2 as the default edition for new sessions\n-- (Existing sessions in V1 continue uninterrupted)\nALTER DATABASE DEFAULT EDITION = v2;\n```\n\nAt this point, new application instances connect using V2. Old application instances running in V1 continue working. Both sets of instances share the same data, with crossedition triggers keeping both column layouts synchronized.\n\n### Phase 5: Retire the Old Edition\n\nOnce all application instances using V1 have been shut down:\n\n```sql\n-- Disable crossedition triggers (no longer needed)\nALTER TRIGGER TRG_CUST_FORWARD DISABLE;\nALTER TRIGGER TRG_CUST_REVERSE DISABLE;\n\n-- Optionally drop them\nDROP TRIGGER TRG_CUST_FORWARD;\nDROP TRIGGER TRG_CUST_REVERSE;\n\n-- Drop the old columns (now that V1 is retired)\nALTER TABLE CUSTOMERS_T DROP COLUMN FULL_NAME;\n\n-- Drop the old edition (cannot drop an edition that has sessions or is the default)\nDROP EDITION v1 CASCADE;\n-- CASCADE drops all editioned objects that existed only in v1\n```\n\n---\n\n## Managing Editions in Practice\n\n### Listing Objects Per Edition\n\n```sql\n-- Which edition does each object belong to?\nSELECT OBJECT_NAME, OBJECT_TYPE, EDITION_NAME, STATUS\nFROM USER_OBJECTS_AE -- AE = All Editions\nWHERE OBJECT_TYPE IN ('PACKAGE', 'PACKAGE BODY', 'VIEW', 'PROCEDURE', 'FUNCTION')\nORDER BY OBJECT_NAME, EDITION_NAME;\n```\n\n### Comparing Object State Across Editions\n\n```sql\n-- Find objects that differ between editions\nSELECT a.OBJECT_NAME, a.OBJECT_TYPE,\n a.EDITION_NAME AS EDITION_A,\n b.EDITION_NAME AS EDITION_B,\n a.LAST_DDL_TIME AS MODIFIED_IN_A,\n b.LAST_DDL_TIME AS MODIFIED_IN_B\nFROM USER_OBJECTS_AE a\nJOIN USER_OBJECTS_AE b\n ON a.OBJECT_NAME = b.OBJECT_NAME\n AND a.OBJECT_TYPE = b.OBJECT_TYPE\n AND a.EDITION_NAME != b.EDITION_NAME\nWHERE a.EDITION_NAME = 'V1'\n AND b.EDITION_NAME = 'V2'\n AND a.LAST_DDL_TIME != b.LAST_DDL_TIME;\n```\n\n### Setting Edition in Connection Strings\n\n```shell\n# JDBC connection string with edition\njdbc:oracle:thin:@//host:1521/service?oracle.jdbc.editionName=V2\n\n# SQL*Plus\nsqlplus app_user/password@//host:1521/service\nALTER SESSION SET EDITION = v2;\n\n# OCI (Python cx_Oracle / oracledb)\nimport oracledb\nconn = oracledb.connect(\n user=\"app_user\",\n password=password,\n dsn=\"host:1521/service\",\n edition=\"V2\"\n)\n```\n\n---\n\n## Use Cases and Limitations\n\n### Ideal Use Cases\n\n- **Rolling deployments** — Deploy new application version to a subset of app servers while old version continues on the remainder, both connected to the same database.\n- **PL/SQL-heavy applications** — EBR shines when the database contains significant business logic in packages; the ability to version packages independently is its primary advantage.\n- **Complex column renames or type changes** — The editioning view + crossedition trigger pattern handles renames cleanly without application downtime.\n- **Automated testing** — Deploy test versions of packages in a dedicated test edition without affecting production sessions.\n\n### Limitations\n\n- **Tables, indexes, and sequences are not editionable.** Structural changes still require careful forward-compatible design (expand/contract).\n- **Cannot use EBR for partitioning or storage changes.** Those require DBMS_REDEFINITION or offline DDL.\n- **DDL complexity increases significantly.** Every object must be created in the correct edition. Missed edition context during deployments causes objects to land in the wrong edition, which can be difficult to debug.\n- **Connection pool management becomes critical.** Connection pools must be configured to specify the correct edition. Pools created without an edition specification default to the database default edition, which may not be the intended edition during a partial rollover.\n- **Cannot drop an edition while it has active sessions or objects that exist only in it.** Plan cleanup procedures carefully.\n- **Not all Oracle features support editioned objects as dependencies.** For example, materialized views cannot reference editioned views.\n\n---\n\n## Best Practices\n\n- **Model editions as a linear chain, not a tree.** While Oracle supports branching edition trees, linear chains (v1 → v2 → v3) are far easier to reason about and operate.\n- **Always set edition context explicitly in deployment scripts.** Never rely on the session default. Begin every deployment script with `ALTER SESSION SET EDITION = target_edition;` and verify with `SELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') FROM DUAL;`.\n- **Keep the number of active editions small (2–3 maximum).** Maintaining more than one previous edition exponentially increases the complexity of crossedition triggers and deployment verification.\n- **Script edition creation and cleanup as part of the pipeline.** Do not rely on manual DBA steps. Create, deploy, switch, and schedule retirement as automated pipeline stages.\n- **Test edition switching in staging with realistic connection pool behavior.** Bugs caused by connection pools using the wrong edition are subtle and reproduce only under load.\n- **Document the current edition state in a deployment runbook.** Operators need to know which edition is current, which is retiring, and which is in staging at all times.\n\n---\n\n## Common Mistakes\n\n**Mistake: Creating objects without setting the edition context first.**\nIf a DBA runs `CREATE OR REPLACE VIEW CUSTOMERS AS ...` without first issuing `ALTER SESSION SET EDITION = v2`, the view is created in the current session edition, which may be the wrong one. Always verify edition context before any DDL in an EBR deployment.\n\n**Mistake: Forgetting that triggers on the base table fire in ALL editions.**\nRegular (non-crossedition) triggers on the base table are not editioned at the trigger level — they fire regardless of the session edition. Only crossedition triggers have edition-specific semantics. Audit triggers, logging triggers, and constraint enforcement triggers on base tables will see all DML from all editions.\n\n**Mistake: Dropping columns before retiring all old-edition sessions.**\nDropping the `FULL_NAME` column while V1 sessions are still active will break those sessions immediately. Always verify that no active sessions are using the old edition before performing column drops.\n\n**Mistake: Using editioning views for non-relational data access.**\nEBR is designed for relational, SQL-based object models. XML DB, Spatial, and other non-relational feature areas have limited or no EBR support.\n\n**Mistake: Conflating EBR with a general-purpose schema versioning tool.**\nEBR manages concurrent code versions. It does not replace schema migration tools like Liquibase or Flyway. The typical production pattern uses both: Flyway/Liquibase for base table DDL changes (additive only, forward-compatible), and EBR for PL/SQL and view versioning during hot rollovers.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database Development Guide 19c — Using Edition-Based Redefinition](https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/editions.html) — EBR introduced in 11gR2; editionable types (SYNONYM, VIEW, PROCEDURE, FUNCTION, PACKAGE, TRIGGER, TYPE, LIBRARY, SQL Translation Profile); crossedition triggers; editioning views\n- [Oracle Database SQL Language Reference 19c — CREATE EDITION](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-EDITION.html) — edition creation and hierarchy\n- [Oracle Database Reference 19c — DBA_EDITIONS](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/DBA_EDITIONS.html) — edition catalog view\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16705,"content_sha256":"342da07fd8d5937aefffcf09bfc5214588cf0c293d01f63a7ea3c589c37d2328"},{"filename":"devops/online-operations.md","content":"# Online Operations in Oracle DB\n\n## Overview\n\nIn production Oracle environments, DDL changes have historically required downtime: locking tables, blocking DML, and preventing reads during structural modifications. Oracle provides a suite of online operation features that allow the database to continue serving application traffic while structural changes take place in the background. Understanding these features — and their limitations — is essential for zero-downtime deployments.\n\nThe key mechanisms are:\n\n- **DBMS_REDEFINITION** — online restructuring of tables with complex changes (column reordering, type changes, partitioning)\n- **Online index operations** — creating or rebuilding indexes without blocking DML\n- **ALTER TABLE ... ONLINE** — adding columns, modifying constraints with reduced locking\n- **Online segment shrink** — reclaiming space without taking tables offline\n\nThese are not universally applicable — each has prerequisites, limitations, and edge cases. This guide explains when and how to use each, with practical examples.\n\n---\n\n## DBMS_REDEFINITION\n\n### What It Does\n\n`DBMS_REDEFINITION` allows you to restructure a table while it remains fully available for DML. The process works by:\n\n1. Creating an interim table with the new structure.\n2. Starting synchronization — Oracle tracks DML applied to the original table in a materialized view log.\n3. Copying existing rows to the interim table in the background.\n4. Periodically synchronizing incremental changes.\n5. Finishing the redefinition — Oracle swaps the table names atomically and drops the original.\n\nApplications see no interruption. The final swap takes a brief exclusive lock (typically milliseconds), but no extended downtime.\n\n### When to Use DBMS_REDEFINITION\n\nUse it when you need to:\n\n- Change column data types (e.g., `VARCHAR2(100)` to `VARCHAR2(500)`, or `DATE` to `TIMESTAMP`)\n- Reorder columns (for compression efficiency or application compatibility)\n- Convert a heap-organized table to a partitioned table\n- Add or remove compression\n- Move a table to a different tablespace online\n- Significantly change the logical structure that `ALTER TABLE` cannot handle\n\n### Prerequisites\n\n```sql\n-- The table must have a primary key (or a unique key you specify as the key_column_list)\n-- The schema owner needs EXECUTE on DBMS_REDEFINITION\n-- The process requires roughly 1x the table size in additional storage during the operation\n\n-- Verify the table can be redefined\nBEGIN\n DBMS_REDEFINITION.CAN_REDEF_TABLE(\n uname => 'APP_OWNER',\n tname => 'ORDERS',\n options_flag => DBMS_REDEFINITION.CONS_USE_PK -- or CONS_USE_ROWID\n );\nEND;\n/\n-- If no exception is raised, the table is eligible\n```\n\n### Full Example: Converting Heap Table to Range-Partitioned\n\n```sql\n-- Step 1: Create the interim table with the new structure\n-- The column names and types must be compatible with the original\n-- Additional columns in the interim table will start as NULL\n\nCREATE TABLE ORDERS_NEW (\n ORDER_ID NUMBER(18,0) NOT NULL,\n CUSTOMER_ID NUMBER(18,0) NOT NULL,\n ORDER_DATE DATE NOT NULL,\n STATUS_CODE VARCHAR2(10) NOT NULL,\n TOTAL_AMOUNT NUMBER(12,4),\n CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,\n CONSTRAINT PK_ORDERS_NEW PRIMARY KEY (ORDER_ID, ORDER_DATE)\n)\nPARTITION BY RANGE (ORDER_DATE)\nINTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))\n(\n PARTITION P_BEFORE_2024 VALUES LESS THAN (DATE '2024-01-01')\n TABLESPACE DATA_TS\n);\n```\n\n```sql\n-- Step 2: Start the redefinition\n-- This creates the materialized view log on ORDERS and begins the copy\nBEGIN\n DBMS_REDEFINITION.START_REDEF_TABLE(\n uname => 'APP_OWNER',\n orig_table => 'ORDERS',\n int_table => 'ORDERS_NEW',\n col_mapping => 'ORDER_ID ORDER_ID, CUSTOMER_ID CUSTOMER_ID, ' ||\n 'ORDER_DATE ORDER_DATE, STATUS_CODE STATUS_CODE, ' ||\n 'TOTAL_AMOUNT TOTAL_AMOUNT, SYSTIMESTAMP CREATED_AT',\n options_flag => DBMS_REDEFINITION.CONS_USE_PK\n );\nEND;\n/\n```\n\n```sql\n-- Step 3: Synchronize incrementally (run one or more times during the copy)\n-- This applies DML that occurred since the last sync, reducing final cutover time\nBEGIN\n DBMS_REDEFINITION.SYNC_INTERIM_TABLE(\n uname => 'APP_OWNER',\n orig_table => 'ORDERS',\n int_table => 'ORDERS_NEW'\n );\nEND;\n/\n```\n\n```sql\n-- Step 4: Copy dependent objects to the interim table\n-- Constraints, indexes, triggers, grants on the interim table become\n-- the constraints/indexes/triggers/grants of the final table after swap\nDECLARE\n v_num_errors PLS_INTEGER;\nBEGIN\n DBMS_REDEFINITION.COPY_TABLE_DEPENDENTS(\n uname => 'APP_OWNER',\n orig_table => 'ORDERS',\n int_table => 'ORDERS_NEW',\n copy_indexes => DBMS_REDEFINITION.CONS_ORIG_PARAMS,\n copy_triggers => TRUE,\n copy_constraints => TRUE,\n copy_privileges => TRUE,\n ignore_errors => FALSE,\n num_errors => v_num_errors\n );\n IF v_num_errors > 0 THEN\n RAISE_APPLICATION_ERROR(-20001, 'COPY_TABLE_DEPENDENTS had errors: ' || v_num_errors);\n END IF;\nEND;\n/\n```\n\n```sql\n-- Step 5: Final synchronization and atomic swap\n-- The original ORDERS table is renamed to ORDERS_OLD (or dropped)\n-- ORDERS_NEW becomes ORDERS\nBEGIN\n DBMS_REDEFINITION.FINISH_REDEF_TABLE(\n uname => 'APP_OWNER',\n orig_table => 'ORDERS',\n int_table => 'ORDERS_NEW'\n );\nEND;\n/\n```\n\n```sql\n-- Step 6: Cleanup\n-- After verifying the new table is correct, drop the old interim\n-- (Oracle renamed ORDERS_NEW to ORDERS and ORDERS to ORDERS_NEW)\nDROP TABLE ORDERS_NEW PURGE;\n```\n\n### Error Recovery\n\nIf the redefinition needs to be aborted at any point:\n\n```sql\nBEGIN\n DBMS_REDEFINITION.ABORT_REDEF_TABLE(\n uname => 'APP_OWNER',\n orig_table => 'ORDERS',\n int_table => 'ORDERS_NEW'\n );\nEND;\n/\n\n-- Then drop the interim table\nDROP TABLE ORDERS_NEW PURGE;\n```\n\n### Monitoring Progress\n\n```sql\n-- Monitor the background copy via MV log and long operations\nSELECT\n SID,\n SERIAL#,\n OPNAME,\n SOFAR,\n TOTALWORK,\n ROUND(SOFAR / NULLIF(TOTALWORK, 0) * 100, 1) AS PCT_COMPLETE,\n TIME_REMAINING AS SECS_REMAINING\nFROM\n V$SESSION_LONGOPS\nWHERE\n OPNAME LIKE '%redefinition%'\n OR OPNAME LIKE '%Table Redefinition%';\n```\n\n---\n\n## Online Index Rebuild\n\n### Why Rebuild Indexes?\n\nIndexes accumulate structural inefficiency over time: deleted entries leave empty leaf blocks, heavy DML on monotonically increasing keys creates right-side imbalance (index blowout), and data movement during updates causes clustering factor degradation. Rebuilding corrects these issues without taking the table offline.\n\n### REBUILD ONLINE vs REBUILD\n\n```sql\n-- Offline rebuild: table DML is blocked during rebuild\nALTER INDEX IDX_ORDERS_CUSTOMER REBUILD;\n\n-- Online rebuild: DML continues, uses a journal table to track changes\n-- Takes longer, more I/O, but does not block application\nALTER INDEX IDX_ORDERS_CUSTOMER REBUILD ONLINE;\n```\n\n```sql\n-- Rebuild with specific storage options\nALTER INDEX IDX_ORDERS_CUSTOMER\nREBUILD ONLINE\nTABLESPACE IDX_TS\nPARALLEL 4\nCOMPUTE STATISTICS;\n```\n\n```sql\n-- Rebuild a partition of a partitioned index\nALTER INDEX IDX_ORDERS_DATE\nREBUILD PARTITION P_2024_Q1 ONLINE;\n```\n\n### Monitoring Index Health Before Deciding to Rebuild\n\n```sql\n-- Analyze index structure\nANALYZE INDEX IDX_ORDERS_CUSTOMER VALIDATE STRUCTURE;\n\nSELECT\n NAME,\n HEIGHT,\n BLOCKS,\n LF_ROWS,\n LF_BLKS,\n LF_ROWS_LEN,\n DEL_LF_ROWS,\n DEL_LF_ROWS_LEN,\n ROUND(DEL_LF_ROWS / NULLIF(LF_ROWS, 0) * 100, 2) AS PCT_DELETED\nFROM\n INDEX_STATS;\n-- If PCT_DELETED > 20% or HEIGHT > 4 for a B-tree, consider rebuilding\n```\n\n```sql\n-- Coalesce (cheaper than rebuild — merges leaf blocks without sorting)\n-- Does not reduce height, does not move to new tablespace\nALTER INDEX IDX_ORDERS_CUSTOMER COALESCE;\n```\n\n---\n\n## Online Index Creation\n\nCreating an index online allows DML to continue on the table during index build. This is the standard approach for adding new indexes to large production tables.\n\n```sql\n-- Standard online index creation\nCREATE INDEX IDX_ORDERS_STATUS\n ON ORDERS (STATUS_CODE, ORDER_DATE)\n ONLINE\n TABLESPACE IDX_TS\n PARALLEL 4\n COMPUTE STATISTICS\n NOLOGGING; -- Reduces redo generation; requires full backup after creation\n\n-- After creation, reset to logging for ongoing maintenance\nALTER INDEX IDX_ORDERS_STATUS LOGGING;\n```\n\n```sql\n-- Unique index online creation\nCREATE UNIQUE INDEX IDX_ORDERS_REF_NUM\n ON ORDERS (REFERENCE_NUMBER)\n ONLINE\n TABLESPACE IDX_TS;\n```\n\n```sql\n-- Function-based index online\nCREATE INDEX IDX_CUST_EMAIL_UPPER\n ON CUSTOMERS (UPPER(EMAIL))\n ONLINE\n TABLESPACE IDX_TS;\n```\n\n### Online Index Limitations\n\n- **Bitmap indexes cannot be created or rebuilt online.** Bitmap indexes are not suitable for OLTP tables with concurrent DML anyway (they lock at the bitmap segment level).\n- **Index on IOT (Index-Organized Table)** secondary indexes cannot always be rebuilt online.\n- **Unusable partitioned index partitions** must be rebuilt offline if the partition is `UNUSABLE`.\n- Online operations consume more undo and temporary space than offline equivalents.\n\n---\n\n## ALTER TABLE ... ONLINE\n\nOracle 12c introduced `ONLINE` clause support for several `ALTER TABLE` operations, reducing lock contention.\n\n### Adding Columns\n\n```sql\n-- Pre-12c: Adding a NOT NULL column with DEFAULT required a full table scan\n-- and a row-by-row update, causing extended locks\n\n-- 12c+: Adding NOT NULL with DEFAULT is metadata-only — instantaneous\nALTER TABLE ORDERS\nADD (LAST_MODIFIED TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL);\n-- Oracle stores the default in the data dictionary; existing rows get the\n-- default value at read time without a physical update\n\n-- Explicitly mark the operation as online (no-op in 12c+ for this case, but explicit)\nALTER TABLE ORDERS ADD (AUDIT_USER VARCHAR2(100)) ONLINE;\n```\n\n### Modifying Column Definitions\n\n```sql\n-- Increase VARCHAR2 column width (always safe, no data conversion)\nALTER TABLE CUSTOMERS MODIFY (EMAIL VARCHAR2(320)) ONLINE;\n\n-- Add a DEFAULT to an existing nullable column\nALTER TABLE ORDERS MODIFY (NOTES VARCHAR2(4000) DEFAULT 'N/A') ONLINE;\n```\n\n### Setting Columns Unused\n\nRather than immediately dropping a column (which rewrites all rows), mark it unused first. The column becomes invisible to the application immediately, and the physical space is reclaimed later.\n\n```sql\n-- Step 1: Make the column invisible to the application (instant)\nALTER TABLE ORDERS SET UNUSED COLUMN LEGACY_REF_NUM;\n\n-- Step 2: Reclaim space during a maintenance window or low-traffic period\nALTER TABLE ORDERS DROP UNUSED COLUMNS;\n\n-- Check for unused columns\nSELECT TABLE_NAME, COUNT(*) AS UNUSED_COLS\nFROM USER_UNUSED_COL_TABS\nGROUP BY TABLE_NAME;\n```\n\n### Online Constraint Operations\n\n```sql\n-- Enable a disabled constraint without validating existing rows (fast, no lock)\nALTER TABLE ORDERS\nENABLE NOVALIDATE CONSTRAINT FK_ORDERS_CUSTOMER;\n\n-- After verifying data quality, enable with full validation\n-- (Does not prevent DML during the validation scan)\nALTER TABLE ORDERS\nENABLE VALIDATE CONSTRAINT FK_ORDERS_CUSTOMER;\n\n-- Add a check constraint that is not validated (future rows only)\nALTER TABLE ORDERS\nADD CONSTRAINT CHK_STATUS CHECK (STATUS_CODE IN ('PENDING','PROCESSING','SHIPPED','CLOSED'))\nENABLE NOVALIDATE;\n```\n\n---\n\n## Online Segment Shrink\n\nShrink reclaims space within a table segment caused by row deletions without moving the table or requiring downtime.\n\n```sql\n-- Step 1: Enable row movement (rows may change ROWID during shrink)\nALTER TABLE ORDERS ENABLE ROW MOVEMENT;\n\n-- Step 2: Compact rows (moves rows, frees space inside blocks; no HWM change yet)\nALTER TABLE ORDERS SHRINK SPACE COMPACT;\n-- At this point DML is not blocked; rows are shifted within the segment\n\n-- Step 3: Adjust High Water Mark (brief exclusive lock, very fast)\nALTER TABLE ORDERS SHRINK SPACE;\n\n-- Alternative: do both steps at once (compact + HWM adjustment)\nALTER TABLE ORDERS SHRINK SPACE CASCADE;\n-- CASCADE also shrinks all dependent indexes\n\n-- After shrinking, disable row movement to re-lock ROWIDs\nALTER TABLE ORDERS DISABLE ROW MOVEMENT;\n```\n\n```sql\n-- Monitor fragmentation before deciding to shrink\nSELECT\n SEGMENT_NAME,\n BLOCKS,\n EMPTY_BLOCKS,\n NUM_ROWS,\n AVG_ROW_LEN,\n ROUND((BLOCKS * 8192) / 1024 / 1024, 2) AS TOTAL_MB,\n ROUND((NUM_ROWS * AVG_ROW_LEN) / 1024 / 1024, 2) AS DATA_MB\nFROM\n USER_TABLES\nWHERE\n SEGMENT_NAME = 'ORDERS';\n```\n\n---\n\n## Minimizing Downtime: Strategy Summary\n\n### Additive Changes (Zero Downtime)\n\nThese changes are always safe to apply while the application is running:\n\n- Adding nullable columns (`ALTER TABLE ADD`)\n- Adding NOT NULL columns with a DEFAULT value (12c+)\n- Creating new indexes (`CREATE INDEX ... ONLINE`)\n- Creating new tables, sequences, synonyms\n- Adding new constraints with `ENABLE NOVALIDATE`\n- Creating or replacing views, packages, procedures (if semantically compatible)\n\n### Destructive or Risky Changes (Require Planning)\n\nThese changes require careful ordering or phased deployment:\n\n| Change | Risk | Mitigation |\n|---|---|---|\n| Dropping a column | Application may still reference it | Set UNUSED first; drop in next release |\n| Renaming a column | Immediate application breakage | Add new column, migrate data, switch app, drop old |\n| Changing column type | May block or fail on large tables | Use DBMS_REDEFINITION |\n| Dropping/truncating a table | Immediate loss | Rename first; drop after application is updated |\n| Converting heap to partitioned | Cannot do with ALTER TABLE | Use DBMS_REDEFINITION |\n\n### The Expand/Contract Pattern\n\nThe safest approach for any non-additive change in a zero-downtime system is the expand/contract (or parallel change) pattern:\n\n1. **Expand** — Add the new structure (new column, new table, new index) alongside the old. Write to both.\n2. **Migrate** — Backfill existing data to the new structure. Verify consistency.\n3. **Contract** — Remove the old structure after all application versions using it have been retired.\n\n---\n\n## Best Practices\n\n- **Always run `DBMS_REDEFINITION.CAN_REDEF_TABLE` before attempting redefinition.** A failure mid-operation on a large table leaves more mess than never starting.\n- **Monitor `V$SESSION_LONGOPS`** during online operations. These operations can run for hours on large tables; visibility into progress is critical.\n- **Plan temporary space.** Online index builds and DBMS_REDEFINITION require additional storage: ~1x the original index size for online rebuild, ~1x the table size for full redefinition.\n- **Rebuild with `PARALLEL` during low-traffic windows.** Even though online operations do not block DML, high-parallelism rebuilds consume significant I/O bandwidth. Reset `NOPARALLEL` after completion.\n- **Use `NOLOGGING` for initial index builds, then `LOGGING` for maintenance.** `NOLOGGING` dramatically reduces redo generation but requires a backup immediately after.\n- **Test online operations in non-production first.** Edge cases (LOB columns, domain indexes, spatial indexes) can prevent online operations from succeeding.\n\n---\n\n## Common Mistakes\n\n**Mistake: Assuming online means instantaneous.**\nOnline operations complete without blocking application DML, but they still consume significant time, I/O, and temp space. A 500 GB table redefinition may take hours. Plan accordingly and monitor progress.\n\n**Mistake: Forgetting to synchronize before FINISH_REDEF_TABLE.**\nThe final swap includes a synchronization step, but calling `SYNC_INTERIM_TABLE` multiple times beforehand reduces the time of the exclusive lock during cutover. Run several manual syncs in the hours before the planned cutover.\n\n**Mistake: Using REBUILD ONLINE on bitmap indexes.**\nOracle will raise `ORA-08106`. Bitmap indexes cannot be created or rebuilt online. They must be rebuilt offline during a maintenance window.\n\n**Mistake: Not disabling ROW MOVEMENT after a shrink.**\nLeaving `ROW MOVEMENT ENABLED` permanently means ROWIDs are not stable. Any application or tool that caches ROWIDs for fast lookup will retrieve incorrect rows after a shrink.\n\n**Mistake: Relying on SET UNUSED for data security.**\n`SET UNUSED COLUMN` makes the column invisible to normal SQL but does not physically remove the data. The column data still exists in the data files. For sensitive data (PII, credentials), use a proper DELETE or UPDATE to nullify the column values before marking it unused.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [DBMS_REDEFINITION (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_REDEFINITION.html) — START_REDEF_TABLE, FINISH_REDEF_TABLE, ABORT_REDEF_TABLE, SYNC_INTERIM_TABLE, COPY_TABLE_DEPENDENTS, CAN_REDEF_TABLE parameters and constants\n- [Oracle Database Administrator's Guide 19c — Online Operations](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-tables.html) — ALTER TABLE ONLINE, shrink, unused columns\n- [Oracle Database SQL Language Reference 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/) — CREATE INDEX ONLINE, REBUILD ONLINE, ALTER TABLE SET UNUSED\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17740,"content_sha256":"c2feb44ad54cd7ab1f88019963f5a18ffbfed62e32e405828b37c5366ab1b695"},{"filename":"devops/schema-migrations.md","content":"# Schema Migrations for Oracle DB\n\n## Overview\n\nSchema migrations are the mechanism by which database structure evolves alongside application code. In a CI/CD pipeline, every DDL change — table creation, column additions, index definitions, constraint changes — must be tracked, versioned, and deployable in a repeatable, auditable way. Ad hoc DDL executed directly against production databases is one of the leading causes of environment drift, deployment failures, and outages.\n\nOracle's character set, data type richness, and procedural extensions (PL/SQL, sequences, triggers, packages) introduce migration-specific challenges that MySQL-centric tools paper over. This guide covers Liquibase and Flyway configured specifically for Oracle, migration strategies, and the DDL lifecycle in modern CI/CD pipelines.\n\n---\n\n## Liquibase with Oracle\n\n### Core Concepts\n\nLiquibase tracks migrations through a **changelog** — a master file that references individual **changesets**. Each changeset is identified by an `id` + `author` + `file` triple, recorded in the `DATABASECHANGELOG` table that Liquibase creates on first run. A `DATABASECHANGELOGLOCK` table prevents concurrent runs.\n\n```\nDATABASECHANGELOG\n ID VARCHAR2(255)\n AUTHOR VARCHAR2(255)\n FILENAME VARCHAR2(255)\n DATEEXECUTED TIMESTAMP\n ORDEREXECUTED NUMBER\n EXECTYPE VARCHAR2(10) -- EXECUTED, FAILED, SKIPPED, RERAN, MARK_RAN\n MD5SUM VARCHAR2(35)\n DESCRIPTION VARCHAR2(255)\n COMMENTS VARCHAR2(255)\n TAG VARCHAR2(255)\n LIQUIBASE VARCHAR2(20)\n CONTEXTS VARCHAR2(255)\n LABELS VARCHAR2(255)\n DEPLOYMENT_ID VARCHAR2(10)\n```\n\n### Project Structure\n\nA well-organized Liquibase project separates concerns by object type:\n\n```\ndb/\n liquibase.properties\n changelog-root.xml\n changes/\n 001-initial-schema.xml\n 002-add-customer-status.xml\n 003-orders-partitioning.xml\n procedures/\n pkg_orders_body.sql\n pkg_orders_spec.sql\n seeds/\n 001-reference-data.xml\n```\n\n### liquibase.properties (Oracle)\n\n```properties\n# liquibase.properties\nurl=jdbc:oracle:thin:@//dbhost:1521/ORCLPDB1\nusername=APP_OWNER\npassword=${DB_PASSWORD}\ndriver=oracle.jdbc.OracleDriver\nchangeLogFile=db/changelog-root.xml\nlogLevel=INFO\ndefaultSchemaName=APP_OWNER\nliquibaseCatalogName=APP_OWNER\n```\n\nFor Oracle Wallets (recommended for CI/CD):\n\n```properties\nurl=jdbc:oracle:thin:@mydb_high?TNS_ADMIN=/opt/wallet\nusername=${DB_USER}\npassword=${DB_PASSWORD}\n```\n\n### Changeset Anatomy\n\n```xml\n\u003c!-- db/changelog-root.xml -->\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cdatabaseChangeLog\n xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog\n http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd\">\n\n \u003cinclude file=\"changes/001-initial-schema.xml\" relativeToChangelogFile=\"true\"/>\n \u003cinclude file=\"changes/002-add-customer-status.xml\" relativeToChangelogFile=\"true\"/>\n \u003cinclude file=\"changes/003-orders-partitioning.xml\" relativeToChangelogFile=\"true\"/>\n\n\u003c/databaseChangeLog>\n```\n\n```xml\n\u003c!-- db/changes/002-add-customer-status.xml -->\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cdatabaseChangeLog\n xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog\n http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd\">\n\n \u003cchangeSet id=\"002\" author=\"jane.smith\" labels=\"customer,status\" context=\"!test\">\n \u003ccomment>Add STATUS column to CUSTOMERS with lookup table\u003c/comment>\n\n \u003c!-- Cross-version Oracle compatibility: use NUMBER(1) instead of SQL BOOLEAN -->\n \u003caddColumn tableName=\"CUSTOMERS\">\n \u003ccolumn name=\"STATUS_CODE\" type=\"VARCHAR2(10)\" defaultValue=\"ACTIVE\">\n \u003cconstraints nullable=\"false\"/>\n \u003c/column>\n \u003c/addColumn>\n\n \u003caddColumn tableName=\"CUSTOMERS\">\n \u003ccolumn name=\"CREATED_AT\" type=\"TIMESTAMP\" defaultValueComputed=\"SYSTIMESTAMP\">\n \u003cconstraints nullable=\"false\"/>\n \u003c/column>\n \u003c/addColumn>\n\n \u003ccreateTable tableName=\"CUSTOMER_STATUS_CODES\">\n \u003ccolumn name=\"CODE\" type=\"VARCHAR2(10)\">\u003cconstraints primaryKey=\"true\" nullable=\"false\"/>\u003c/column>\n \u003ccolumn name=\"DESCRIPTION\" type=\"VARCHAR2(100)\">\u003cconstraints nullable=\"false\"/>\u003c/column>\n \u003ccolumn name=\"IS_ACTIVE\" type=\"NUMBER(1,0)\" defaultValueNumeric=\"1\">\u003cconstraints nullable=\"false\"/>\u003c/column>\n \u003c/createTable>\n\n \u003cinsert tableName=\"CUSTOMER_STATUS_CODES\">\n \u003ccolumn name=\"CODE\" value=\"ACTIVE\"/>\n \u003ccolumn name=\"DESCRIPTION\" value=\"Active customer\"/>\n \u003ccolumn name=\"IS_ACTIVE\" valueNumeric=\"1\"/>\n \u003c/insert>\n\n \u003cinsert tableName=\"CUSTOMER_STATUS_CODES\">\n \u003ccolumn name=\"CODE\" value=\"SUSPENDED\"/>\n \u003ccolumn name=\"DESCRIPTION\" value=\"Account suspended\"/>\n \u003ccolumn name=\"IS_ACTIVE\" valueNumeric=\"1\"/>\n \u003c/insert>\n\n \u003caddForeignKeyConstraint\n baseTableName=\"CUSTOMERS\"\n baseColumnNames=\"STATUS_CODE\"\n referencedTableName=\"CUSTOMER_STATUS_CODES\"\n referencedColumnNames=\"CODE\"\n constraintName=\"FK_CUST_STATUS\"/>\n\n \u003crollback>\n \u003cdropForeignKeyConstraint baseTableName=\"CUSTOMERS\" constraintName=\"FK_CUST_STATUS\"/>\n \u003cdropColumn tableName=\"CUSTOMERS\" columnName=\"STATUS_CODE\"/>\n \u003cdropColumn tableName=\"CUSTOMERS\" columnName=\"CREATED_AT\"/>\n \u003cdropTable tableName=\"CUSTOMER_STATUS_CODES\"/>\n \u003c/rollback>\n \u003c/changeSet>\n\n\u003c/databaseChangeLog>\n```\n\n### Oracle-Specific Data Types in Liquibase\n\nLiquibase's generic types do not always map cleanly to Oracle. For mixed-version estates, prefer explicit Oracle-native types:\n\n| Generic Liquibase | Oracle Reality | Use Instead |\n|---|---|---|\n| `BOOLEAN` | SQL BOOLEAN exists in 23ai/26ai, but older estates and some toolchains still expect numeric compatibility | `NUMBER(1,0)` with CHECK (col IN (0,1)) for cross-version compatibility |\n| `BIGINT` | Maps to `NUMBER(19,0)` | `NUMBER(18,0)` or `NUMBER(19,0)` explicitly |\n| `DATETIME` | Maps to `DATE` (loses sub-second) | `TIMESTAMP` or `TIMESTAMP WITH TIME ZONE` |\n| `TEXT` | Maps to `CLOB` | `VARCHAR2(4000)` or `CLOB` depending on need |\n| `INT` | Maps to `NUMBER(10,0)` | `NUMBER(10,0)` explicitly |\n\n```xml\n\u003c!-- Prefer explicit Oracle types -->\n\u003ccolumn name=\"PRICE\" type=\"NUMBER(12,4)\"/>\n\u003ccolumn name=\"CREATED_AT\" type=\"TIMESTAMP WITH LOCAL TIME ZONE\"/>\n\u003ccolumn name=\"NOTES\" type=\"CLOB\"/>\n\u003ccolumn name=\"IS_ENABLED\" type=\"NUMBER(1,0)\"/>\n```\n\n### Sequences and Triggers with Liquibase\n\nPre-Oracle 12c, identity columns did not exist. Even post-12c, many codebases use sequence+trigger patterns.\n\n```xml\n\u003c!-- Oracle 12c+ identity column -->\n\u003cchangeSet id=\"010\" author=\"dev\">\n \u003ccreateTable tableName=\"ORDERS\">\n \u003ccolumn name=\"ORDER_ID\" type=\"NUMBER(18,0)\" autoIncrement=\"true\"\n generationType=\"ALWAYS\">\n \u003cconstraints primaryKey=\"true\" nullable=\"false\"/>\n \u003c/column>\n \u003ccolumn name=\"ORDER_DATE\" type=\"DATE\"/>\n \u003c/createTable>\n\u003c/changeSet>\n\n\u003c!-- Pre-12c: explicit sequence + trigger -->\n\u003cchangeSet id=\"010b\" author=\"dev\" dbms=\"oracle\">\n \u003ccreateSequence sequenceName=\"SEQ_ORDERS\"\n startValue=\"1\"\n incrementBy=\"1\"\n minValue=\"1\"\n maxValue=\"9999999999999999999\"\n cycle=\"false\"\n ordered=\"false\"\n cacheSize=\"20\"/>\n\n \u003c!-- Use sqlFile for complex PL/SQL -->\n \u003csqlFile path=\"triggers/trg_orders_bi.sql\"\n relativeToChangelogFile=\"false\"\n splitStatements=\"false\"\n endDelimiter=\"/\"\n stripComments=\"false\"/>\n\n \u003crollback>\n \u003csql>DROP TRIGGER TRG_ORDERS_BI\u003c/sql>\n \u003cdropSequence sequenceName=\"SEQ_ORDERS\"/>\n \u003c/rollback>\n\u003c/changeSet>\n```\n\n```sql\n-- triggers/trg_orders_bi.sql\nCREATE OR REPLACE TRIGGER TRG_ORDERS_BI\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\nWHEN (NEW.ORDER_ID IS NULL)\nBEGIN\n :NEW.ORDER_ID := SEQ_ORDERS.NEXTVAL;\nEND;\n/\n```\n\n### Running Liquibase\n\n```shell\n# Preview changes without executing (generates SQL to stdout)\nliquibase updateSQL\n\n# Apply pending changesets\nliquibase update\n\n# Apply only changesets tagged up to a specific point\nliquibase updateToTag v2.3.0\n\n# Tag the current state\nliquibase tag v2.3.0\n\n# Roll back to a tag\nliquibase rollback v2.3.0\n\n# Roll back a specific count of changesets\nliquibase rollbackCount 3\n\n# Check current status\nliquibase status --verbose\n\n# Validate changelog syntax\nliquibase validate\n```\n\n---\n\n## Flyway with Oracle\n\n### Key Differences from Liquibase\n\nFlyway uses a simpler, file-name-driven versioning scheme. Migration files are named `V{version}__{description}.sql` for versioned migrations and `R__{description}.sql` for repeatable migrations. There is no XML DSL — everything is raw SQL or Java callbacks.\n\n```\ndb/migration/\n V1__initial_schema.sql\n V2__add_customer_status.sql\n V2.1__customer_status_fk.sql\n V3__orders_table.sql\n R__vw_active_customers.sql -- repeatable\n R__pkg_order_processing.sql -- repeatable (PL/SQL package)\n```\n\n### flyway.toml (Oracle)\n\n```toml\n[environments.default]\nurl = \"jdbc:oracle:thin:@//dbhost:1521/ORCLPDB1\"\nuser = \"${DB_USER}\"\npassword = \"${DB_PASSWORD}\"\nschemas = [\"APP_OWNER\"]\n\n[flyway]\nlocations = [\"filesystem:db/migration\"]\ndefaultSchema = \"APP_OWNER\"\nvalidateOnMigrate = true\noutOfOrder = false\n# Required for PL/SQL blocks that end with /\nsqlMigrationSuffixes = [\".sql\"]\nplaceholderReplacement = false\n```\n\n### Oracle PL/SQL in Flyway\n\nFlyway's default statement delimiter is `;`. PL/SQL blocks require `/` as the terminator. Configure this per-migration using the special comment annotation:\n\n```sql\n-- V4__create_order_package.sql\n-- flyway:delimiter=/\n\nCREATE OR REPLACE PACKAGE PKG_ORDERS AS\n PROCEDURE process_order(p_order_id IN NUMBER);\n FUNCTION get_order_total(p_order_id IN NUMBER) RETURN NUMBER;\nEND PKG_ORDERS;\n/\n\nCREATE OR REPLACE PACKAGE BODY PKG_ORDERS AS\n\n PROCEDURE process_order(p_order_id IN NUMBER) IS\n v_status VARCHAR2(10);\n BEGIN\n SELECT STATUS_CODE INTO v_status\n FROM ORDERS\n WHERE ORDER_ID = p_order_id;\n\n IF v_status = 'PENDING' THEN\n UPDATE ORDERS\n SET STATUS_CODE = 'PROCESSING',\n PROCESSED_AT = SYSTIMESTAMP\n WHERE ORDER_ID = p_order_id;\n END IF;\n\n COMMIT;\n END process_order;\n\n FUNCTION get_order_total(p_order_id IN NUMBER) RETURN NUMBER IS\n v_total NUMBER;\n BEGIN\n SELECT SUM(LINE_TOTAL)\n INTO v_total\n FROM ORDER_LINES\n WHERE ORDER_ID = p_order_id;\n RETURN NVL(v_total, 0);\n END get_order_total;\n\nEND PKG_ORDERS;\n/\n```\n\n### Repeatable Migrations\n\nRepeatable migrations re-run whenever their checksum changes. They are ideal for views, packages, synonyms, and grants — objects you want to fully replace rather than alter incrementally.\n\n```sql\n-- R__vw_active_customers.sql\n-- This re-runs any time the file content changes\n\nCREATE OR REPLACE VIEW VW_ACTIVE_CUSTOMERS AS\nSELECT\n c.CUSTOMER_ID,\n c.FIRST_NAME,\n c.LAST_NAME,\n c.EMAIL,\n c.STATUS_CODE,\n cs.DESCRIPTION AS STATUS_DESC,\n c.CREATED_AT\nFROM\n CUSTOMERS c\nJOIN\n CUSTOMER_STATUS_CODES cs ON cs.CODE = c.STATUS_CODE\nWHERE\n c.STATUS_CODE != 'CLOSED';\n```\n\n```sql\n-- R__pkg_order_processing.sql\n-- flyway:delimiter=/\n\nCREATE OR REPLACE PACKAGE BODY PKG_ORDER_PROCESSING AS\n -- Full package body here; re-deployed on any change\nEND PKG_ORDER_PROCESSING;\n/\n```\n\n### Running Flyway\n\n```shell\n# Check migration status\nflyway info\n\n# Apply pending migrations\nflyway migrate\n\n# Validate checksums (detects manual edits to applied migrations)\nflyway validate\n\n# Repair checksum mismatches (use carefully — only for broken environments)\nflyway repair\n\n# Undo last versioned migration (requires Flyway Teams)\nflyway undo\n\n# Clean schema (NEVER in production — drops all objects)\nflyway clean\n```\n\n---\n\n## Versioned vs Repeatable Migrations\n\n| Aspect | Versioned (V prefix) | Repeatable (R prefix) |\n|---|---|---|\n| Execution | Once, in version order | On every checksum change |\n| Rollback | Requires explicit undo migration | Simply revert file content |\n| Use cases | Table/column DDL, data backfills | Views, packages, synonyms, grants |\n| Ordering | Sequential, enforced | Runs after all versioned migrations |\n| Collision | Two same-version files = error | Re-runs are idempotent |\n\n### Guidelines\n\n- DDL that modifies existing tables (ALTER TABLE) must be versioned — it cannot be repeated idempotently.\n- Views, materialized view definitions, packages, procedures, and functions should be repeatable. This eliminates the common trap of having a `V12__fix_view.sql` that duplicates most of `V8__create_view.sql`.\n- Reference data inserts should be versioned or use `MERGE` within repeatable migrations to achieve idempotency.\n\n---\n\n## Managing DDL in CI/CD Pipelines\n\n### Pipeline Design\n\n```yaml\n# .github/workflows/db-deploy.yml\nname: Database Deploy\n\non:\n push:\n branches: [main]\n paths:\n - 'db/**'\n\njobs:\n validate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Validate Liquibase Changelog\n run: |\n liquibase \\\n --url=\"${{ secrets.DB_DEV_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n validate\n\n deploy-dev:\n needs: validate\n runs-on: ubuntu-latest\n environment: dev\n steps:\n - uses: actions/checkout@v4\n\n - name: Generate migration SQL (artifact for review)\n run: |\n liquibase \\\n --url=\"${{ secrets.DB_DEV_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n updateSQL > migration-dev.sql\n\n - uses: actions/upload-artifact@v4\n with:\n name: migration-sql-dev\n path: migration-dev.sql\n\n - name: Apply migrations to DEV\n run: |\n liquibase \\\n --url=\"${{ secrets.DB_DEV_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n update\n\n deploy-prod:\n needs: deploy-dev\n runs-on: ubuntu-latest\n environment: production\n steps:\n - uses: actions/checkout@v4\n\n - name: Apply migrations to PROD\n run: |\n liquibase \\\n --url=\"${{ secrets.DB_PROD_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n update\n```\n\n### Locking and Concurrency\n\nBoth Liquibase and Flyway use a lock table to prevent concurrent migration runs. In CI/CD environments with multiple parallel jobs or failed deployments, stale locks are common:\n\n```sql\n-- Liquibase: check and release a stale lock\nSELECT * FROM DATABASECHANGELOGLOCK;\n\nUPDATE DATABASECHANGELOGLOCK\nSET LOCKED = 0,\n LOCKGRANTED = NULL,\n LOCKEDBY = NULL\nWHERE ID = 1;\n\nCOMMIT;\n```\n\n```shell\n# Flyway: release stale lock\nflyway repair\n```\n\n### Environment-Specific Configuration\n\nNever embed credentials in changelogs. Use environment variables or vault references:\n\n```shell\n# Shell pattern using environment variables\nexport LIQUIBASE_COMMAND_URL=\"jdbc:oracle:thin:@//${DB_HOST}:${DB_PORT}/${DB_SERVICE}\"\nexport LIQUIBASE_COMMAND_USERNAME=\"${DB_USER}\"\nexport LIQUIBASE_COMMAND_PASSWORD=\"${DB_PASSWORD}\"\nliquibase update\n```\n\n---\n\n## Handling Sequences in Migrations\n\nSequences require special care because their current value is stateful — unlike structural DDL, a sequence's `LAST_NUMBER` cannot simply be rolled back.\n\n```sql\n-- Creating a sequence with Oracle best practices\nCREATE SEQUENCE SEQ_INVOICE_ID\n START WITH 1\n INCREMENT BY 1\n MINVALUE 1\n MAXVALUE 9999999999999999999\n NOCYCLE\n CACHE 100 -- Cache 100 values in memory; balance performance vs gap size\n NOORDER; -- ORDER only needed for RAC when strict ordering is required\n```\n\n```xml\n\u003c!-- Liquibase: sequence creation with rollback -->\n\u003cchangeSet id=\"020\" author=\"dev\" dbms=\"oracle\">\n \u003ccreateSequence\n sequenceName=\"SEQ_INVOICE_ID\"\n startValue=\"1\"\n incrementBy=\"1\"\n cacheSize=\"100\"\n cycle=\"false\"\n ordered=\"false\"/>\n \u003crollback>\n \u003cdropSequence sequenceName=\"SEQ_INVOICE_ID\"/>\n \u003c/rollback>\n\u003c/changeSet>\n```\n\n### Resetting Sequences in Non-Production\n\nA common need is resetting sequences after data refreshes in lower environments:\n\n```sql\n-- Reset sequence to 1 (Oracle 18c+)\nALTER SEQUENCE SEQ_INVOICE_ID RESTART START WITH 1;\n\n-- Pre-18c workaround: drop and recreate\nDECLARE\n v_current NUMBER;\n v_stmt VARCHAR2(200);\nBEGIN\n SELECT LAST_NUMBER INTO v_current\n FROM USER_SEQUENCES\n WHERE SEQUENCE_NAME = 'SEQ_INVOICE_ID';\n\n -- Step down by current value to effectively reset\n v_stmt := 'ALTER SEQUENCE SEQ_INVOICE_ID INCREMENT BY -' || v_current || ' MINVALUE 0';\n EXECUTE IMMEDIATE v_stmt;\n EXECUTE IMMEDIATE 'SELECT SEQ_INVOICE_ID.NEXTVAL FROM DUAL';\n EXECUTE IMMEDIATE 'ALTER SEQUENCE SEQ_INVOICE_ID INCREMENT BY 1 MINVALUE 1';\nEND;\n/\n```\n\n---\n\n## Best Practices\n\n- **Never edit an applied versioned migration.** Both tools detect checksum changes. If a deployed migration has a bug, write a new corrective migration. Reserve `flyway repair` or `liquibase changelogSync` only for truly broken environments where the change was already applied manually.\n- **Separate DDL owner from application user.** Run migrations as a schema owner (or DBA-equivalent) account. The application runtime user should have only DML privileges. This prevents accidental DDL from application bugs.\n- **Keep changesets small and focused.** One logical change per changeset. Large changesets are hard to roll back, hard to diagnose, and block other schema operations with long-held DDL locks.\n- **Always write rollback blocks.** Liquibase does not auto-generate rollback SQL for many Oracle DDL operations. Write explicit `\u003crollback>` blocks even if you never plan to use them — they serve as documentation.\n- **Use contexts and labels** to control which changesets run in which environments. Seed data, test fixtures, and performance-heavy backfills should be excluded from production runs.\n- **Run `updateSQL` before `update` in production.** The generated SQL file becomes an artifact for DBA review, audit logs, and post-incident analysis.\n- **Avoid `flyway clean` on shared environments.** It drops all objects. It should only be used in fully isolated development environments or ephemeral CI containers.\n\n---\n\n## Common Mistakes\n\n**Mistake: Using `BOOLEAN` as a column type.**\nOracle 23ai/26ai support SQL `BOOLEAN`, but many Oracle estates still target 19c compatibility and some migration toolchains normalize booleans to numeric columns. For cross-version portability, use `NUMBER(1,0)` explicitly with a CHECK constraint unless you are intentionally targeting 23ai/26ai-only schemas.\n\n```sql\n-- Wrong (ambiguous mapping)\n\u003ccolumn name=\"IS_ACTIVE\" type=\"BOOLEAN\"/>\n\n-- Right\n\u003ccolumn name=\"IS_ACTIVE\" type=\"NUMBER(1,0)\" defaultValueNumeric=\"1\">\n \u003cconstraints nullable=\"false\" checkConstraint=\"IS_ACTIVE IN (0, 1)\"/>\n\u003c/column>\n```\n\n**Mistake: Committing `splitStatements=true` with PL/SQL.**\nLiquibase splits statements on `;` by default. PL/SQL blocks contain many semicolons. Always set `splitStatements=\"false\"` and use `endDelimiter=\"/\"` for PL/SQL files.\n\n**Mistake: Including `GRANT` statements in versioned migrations.**\nGrants applied via versioned migrations are re-applied once and then forgotten. If a user is dropped and recreated, the grant is lost. Use repeatable migrations or a separate grants script that is idempotent.\n\n**Mistake: Not testing rollbacks.**\nRollbacks are only useful if they work. Include rollback testing in CI by applying a migration set, then rolling back, and verifying the schema matches the pre-migration baseline.\n\n**Mistake: Running migrations synchronously with application deployment.**\nIn high-availability deployments, the application and database versions may be temporarily mismatched. Design migrations to be backward-compatible: add columns as nullable before backfilling, do not drop columns until the old application version is fully retired.\n\n---\n\n\n## Security Considerations\n\n### Privilege Management for Migration Accounts\nMigration processes often require elevated privileges, creating security risks if not properly managed:\n\n- **Use least privilege principle for migration accounts:**\n ```sql\n -- Instead of granting DBA role (excessive privileges):\n -- GRANT DBA TO migration_user; -- AVOID\n\n -- Grant only specific privileges needed for schema changes:\n CREATE USER migration_user IDENTIFIED BY \"strong_password\";\n GRANT CREATE SESSION TO migration_user;\n GRANT CREATE TABLE, ALTER TABLE, DROP TABLE TO migration_user;\n GRANT CREATE VIEW, ALTER VIEW, DROP VIEW TO migration_user;\n GRANT CREATE PROCEDURE, ALTER PROCEDURE, DROP PROCEDURE TO migration_user;\n GRANT CREATE TRIGGER, ALTER TRIGGER, DROP TRIGGER TO migration_user;\n GRANT CREATE SEQUENCE, ALTER SEQUENCE, DROP SEQUENCE TO migration_user;\n GRANT CREATE INDEX, ALTER INDEX, DROP INDEX TO migration_user;\n -- Add privileges for specific schemas if needed\n GRANT CREATE ANY TABLE TO migration_user; -- Only if absolutely necessary\n GRANT CREATE ANY INDEX TO migration_user; -- Only if absolutely necessary\n ```\n\n- **Separate migration user from application user:**\n - Migration user: Has DDL privileges to modify schema\n - Application user: Has only DML privileges (SELECT, INSERT, UPDATE, DELETE) on application objects\n - Never use the same credentials for both purposes\n\n- **Consider using role-based access control:**\n ```sql\n CREATE ROLE schema_migration_role;\n GRANT CREATE TABLE, ALTER TABLE, DROP TABLE TO schema_migration_role;\n GRANT CREATE VIEW, ALTER VIEW, DROP VIEW TO schema_migration_role;\n GRANT CREATE PROCEDURE, ALTER PROCEDURE, DROP PROCEDURE TO schema_migration_role;\n GRANT CREATE TRIGGER, ALTER TRIGGER, DROP TRIGGER TO schema_migration_role;\n GRANT CREATE SEQUENCE, ALTER SEQUENCE, DROP SEQUENCE TO schema_migration_role;\n GRANT CREATE INDEX, ALTER INDEX, DROP INDEX TO schema_migration_role;\n GRANT schema_migration_role TO migration_user;\n ```\n\n### Secure Credential Handling in CI/CD\nMigration tools often require database credentials, which must be protected in CI/CD pipelines:\n\n- **Never hardcode credentials in migration files or configuration:**\n ```xml\n \u003c!-- AVOID: Hardcoded credentials in changelog -->\n \u003cdatabaseChangeLog\n xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n username=\"hardcoded_user\"\n password=\"hardcoded_password\">\n ```\n\n- **Use environment variables or secret management systems:**\n ```bash\n # Liquibase with environment variables (CI/CD safe)\n liquibase \\\n --url=\"${{ secrets.DB_PROD_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n update\n ```\n\n ```toml\n # Flyway TOML with environment variables\n [environments.default]\n url = \"${DB_PROD_URL}\"\n user = \"${DB_USER}\"\n password = \"${DB_PASSWORD}\"\n ```\n\n- **Integrate with enterprise secret stores:**\n - HashiCorp Vault, AWS Secrets Manager, Azure Key Vault\n - Use CI/CD plugin integrations to fetch secrets at runtime\n - Never store secrets in repository history\n\n### Protecting Sensitive Data During Migrations\nSchema migrations can expose or mishandle sensitive data if not properly controlled:\n\n- **Be cautious with data migrations involving sensitive information:**\n ```sql\n -- Example: Migrating to encrypted columns\n BEGIN\n -- Add encrypted column\n ALTER TABLE customers ADD (ssn_encrypted VARCHAR2(255));\n\n -- Encrypt existing data (in batches to avoid locks)\n UPDATE customers\n SET ssn_encrypted = ENCRYPT_USING(ssn, 'AES256', :encryption_key)\n WHERE ssn IS NOT NULL;\n\n -- Verify encryption worked\n -- Drop original column only after verification\n ALTER TABLE customers DROP COLUMN ssn;\n END;\n ```\n\n- **Use secure techniques for handling sensitive data:**\n - Process sensitive data in small batches to minimize exposure\n - Use encryption keys from secure wallets/HSMs, not hardcoded values\n - Consider using Oracle Data Redaction during migration windows\n - Log access to sensitive data during migration processes\n\n- **Never log or expose sensitive data in migration scripts:**\n ```sql\n -- AVOID: Logging sensitive values\n INSERT INTO migration_log (step, details)\n VALUES ('ENCRYPT_SSN', 'Encrypting SSN: ' || ssn_value); -- EXPOSES SENSITIVE DATA\n\n -- INSTEAD: Log only non-sensitive metadata\n INSERT INTO migration_log (step, details, record_count)\n VALUES ('ENCRYPT_SSN', 'SSN encryption completed', SQL%ROWCOUNT);\n ```\n\n### Audit and Compliance for Schema Changes\nSchema changes themselves should be audited for compliance and security monitoring:\n\n- **Enable DDL auditing to track schema modifications:**\n ```sql\n -- Audit all DDL changes in critical schemas\n CREATE AUDIT POLICY schema_changes_audit\n ACTIONS CREATE TABLE, ALTER TABLE, DROP TABLE,\n CREATE VIEW, ALTER VIEW, DROP VIEW,\n CREATE PROCEDURE, ALTER PROCEDURE, DROP PROCEDURE,\n CREATE TRIGGER, ALTER TRIGGER, DROP TRIGGER,\n CREATE SEQUENCE, DROP SEQUENCE,\n CREATE INDEX, DROP INDEX;\n AUDIT POLICY schema_changes_audit BY USERS WITH GRANTED ROLES;\n ```\n\n- **Monitor for unauthorized schema changes:**\n ```sql\n -- Alert on DDL outside of approved maintenance windows\n CREATE AUDIT POLICY unauthorized_ddl_alert\n ACTIONS CREATE TABLE, ALTER TABLE, DROP TABLE\n WHEN 'NOT (TO_NUMBER(TO_CHAR(SYSDATE,''HH24MISS'')) BETWEEN 20000 AND 40000)'\n EVALUATE PER SESSION;\n AUDIT POLICY unauthorized_ddl_alert;\n ```\n\n- **Maintain immutable audit trail for compliance:**\n - Ensure audit records cannot be altered or deleted by migration users\n - Use unified auditing with separate AUDSYS schema\n - Regularly archive audit logs to secure, write-once storage\n\n### Securing Migration Artifacts\nMigration-generated SQL files can contain sensitive information:\n\n- **Treat generated SQL as potentially sensitive:**\n - May contain table structures, index definitions, and data values\n - Restrict access to migration artifacts (updateSQL output)\n - Delete temporary SQL files after use in CI/CD pipelines\n\n- **Secure handling of migration SQL artifacts:**\n ```yaml\n # In CI/CD pipeline - secure handling of generated SQL\n - name: Generate migration SQL (artifact for review)\n run: |\n liquibase \\\n --url=\"${{ secrets.DB_PROD_URL }}\" \\\n --username=\"${{ secrets.DB_USER }}\" \\\n --password=\"${{ secrets.DB_PASSWORD }}\" \\\n updateSQL > migration-prod.sql\n - name: Upload encrypted artifact\n run: |\n # Encrypt before uploading if containing sensitive data\n gpg --symmetric --cipher-algo AES256 migration-prod.sql\n rm migration-prod.sql # Delete plaintext version\n - uses: actions/upload-artifact@v4\n with:\n name: encrypted-migration-sql\n path: migration-prod.sql.gpg\n ```\n\n### Database Link Security in Migrations\n\n- **Secure database link usage in migration scripts:**\n ```sql\n -- AVOID: Hardcoded credentials in database links\n CREATE DATABASE LINK remote_db\n CONNECT TO remote_user IDENTIFIED BY \"hardcoded_password\"\n USING 'remote_database';\n\n -- INSTEAD: Use external password store or OS authentication\n CREATE DATABASE LINK secure_remote_db\n CONNECT TO CURRENT_USER USING 'remote_database';\n -- Or use wallet-based authentication\n ```\n\n- **Monitor and audit database link usage:**\n ```sql\n CREATE AUDIT POLICY db_link_usage\n ACTIONS EXECUTE ON SYS.DBMS_SQL;\n AUDIT POLICY db_link_usage;\n ```\n\n### Migration Rollback Security Considerations\n\n- **Test rollback procedures in non-production first:**\n - Verify rollback scripts don't leave data in inconsistent state\n - Confirm sensitive data is properly handled during rollback\n\n- **Be cautious with data loss during rollback:**\n ```sql\n -- Example: Adding then dropping a column\n -- Migration: ADD COLUMN ssn VARCHAR2(11)\n -- Rollback: DROP COLUMN ssn\n --\n -- If rollback is executed after data has been added:\n -- THE SSN DATA IS PERMANENTLY LOST\n --\n -- Better approach for sensitive data:\n -- Migration: ADD COLUMN ssn_encrypted VARCHAR2(255) (encrypted)\n -- Migration: COPY AND ENCRYPT DATA FROM ssn TO ssn_encrypted\n -- Migration: DROP COLUMN ssn (after verification)\n -- Rollback: RECOVERY PROCEDURE NEEDED (not simple DROP)\n ```\n\n### Compliance-Specific Migration Requirements\n\n- **PCI-DSS Requirement 6.4**:\n - Restrict access to cardholder data environments\n - Implement change detection mechanisms for schema changes\n - Separate development/test and production environments\n\n- **SOX Section 404**:\n - Implement change management controls for financial systems\n - Document and approve all schema changes\n - Maintain audit trail of all DDL changes\n\n- **GDPR Article 25 (Data Protection by Design)**:\n - Consider data minimization when adding new columns\n - Implement pseudonymization techniques where appropriate\n - Ensure right to erasure can be implemented through schema design\n\n### Secure Migration Lifecycle\n\n- **Development**: Use encrypted credentials, test on anonymized data\n- **CI/CD**: Secure credential handling, artifact protection, approval gates\n- **Staging**: Validate against production-like data with masking\n- **Production**: Change windows, monitoring, rollback procedures tested\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Liquibase Documentation](https://docs.liquibase.com/) — changelog structure, changeset anatomy, Oracle data type mappings, rollback\n- [Flyway Documentation](https://documentation.red-gate.com/flyway) — versioned vs. repeatable migrations, flyway.toml, PL/SQL delimiter configuration\n- [Oracle Database SQL Language Reference 19c — CREATE SEQUENCE](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html) — CACHE, NOORDER, RESTART (18c+)\n- [Oracle Database Development Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/adfns/) — Oracle-specific migration considerations\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":30736,"content_sha256":"e7b5c6f2f0792453b061b89dbde21a3ea78f657f2fe56584470ea3af3be69ad9"},{"filename":"devops/version-control-sql.md","content":"# Version Control for Oracle SQL and Schema Objects\n\n## Overview\n\nPutting an Oracle schema under version control is not just about backing up DDL — it is about creating a single source of truth for the database structure that enables code review, rollback, environment comparison, automated deployment, and historical auditing. Done well, every object in the schema has a corresponding definition file in git, and the database can be fully reconstructed from the repository.\n\nThe challenges are Oracle-specific: DDL generated by Oracle tools is verbose and inconsistent, objects have interdependencies that impose a deployment order, grants and synonyms are often overlooked, and PL/SQL bodies may differ from the stored definition due to manual hotfixes. This guide covers extracting DDL accurately, organizing it in git, scripting grants and synonyms, and integrating with SQL Developer's source control features.\n\n---\n\n## Extracting DDL with DBMS_METADATA\n\n`DBMS_METADATA` is Oracle's built-in package for generating DDL from the data dictionary. It is far more reliable than reverse-engineering from third-party tools, because it directly reads the same internal representation Oracle uses.\n\n### Basic Usage\n\n```sql\n-- Extract DDL for a single table\nSELECT DBMS_METADATA.GET_DDL('TABLE', 'ORDERS', 'APP_OWNER') FROM DUAL;\n\n-- Extract DDL for a single index\nSELECT DBMS_METADATA.GET_DDL('INDEX', 'IDX_ORDERS_CUSTOMER', 'APP_OWNER') FROM DUAL;\n\n-- Extract DDL for a package spec and body\nSELECT DBMS_METADATA.GET_DDL('PACKAGE', 'PKG_ORDERS', 'APP_OWNER') FROM DUAL;\nSELECT DBMS_METADATA.GET_DDL('PACKAGE_BODY', 'PKG_ORDERS', 'APP_OWNER') FROM DUAL;\n\n-- Supported object types\n-- TABLE, INDEX, VIEW, SEQUENCE, PROCEDURE, FUNCTION,\n-- PACKAGE, PACKAGE_BODY, TRIGGER, TYPE, TYPE_BODY,\n-- SYNONYM, DB_LINK, CONSTRAINT, REF_CONSTRAINT, GRANT\n```\n\n### Configuring Output Format\n\nThe default DDL output includes storage clauses, tablespace names, and physical attributes that create noise in version control. Transform the output to remove environment-specific details:\n\n```sql\nBEGIN\n -- Remove storage clauses (STORAGE (...))\n DBMS_METADATA.SET_TRANSFORM_PARAM(\n transform_handle => DBMS_METADATA.SESSION_TRANSFORM,\n name => 'STORAGE',\n value => FALSE\n );\n\n -- Remove tablespace specifications\n DBMS_METADATA.SET_TRANSFORM_PARAM(\n transform_handle => DBMS_METADATA.SESSION_TRANSFORM,\n name => 'TABLESPACE',\n value => FALSE\n );\n\n -- Remove segment attributes (PCTFREE, INITRANS, etc.)\n DBMS_METADATA.SET_TRANSFORM_PARAM(\n transform_handle => DBMS_METADATA.SESSION_TRANSFORM,\n name => 'SEGMENT_ATTRIBUTES',\n value => FALSE\n );\n\n -- Add a terminating semicolon to each statement\n DBMS_METADATA.SET_TRANSFORM_PARAM(\n transform_handle => DBMS_METADATA.SESSION_TRANSFORM,\n name => 'SQLTERMINATOR',\n value => TRUE\n );\n\n -- Retain pretty-printing\n DBMS_METADATA.SET_TRANSFORM_PARAM(\n transform_handle => DBMS_METADATA.SESSION_TRANSFORM,\n name => 'PRETTY',\n value => TRUE\n );\nEND;\n/\n\n-- Now extract clean DDL\nSELECT DBMS_METADATA.GET_DDL('TABLE', 'ORDERS', 'APP_OWNER') FROM DUAL;\n```\n\n### Bulk DDL Extraction Script\n\n```sql\n-- Extract all tables, then all indexes, then all views\n-- Run as the schema owner or a DBA\n\nSET LONG 999999\nSET PAGESIZE 0\nSET LINESIZE 200\nSET FEEDBACK OFF\nSET HEADING OFF\nSET TRIMSPOOL ON\n\nSPOOL /tmp/extract_tables.sql\n\nBEGIN\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'STORAGE', FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'TABLESPACE', FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES',FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SQLTERMINATOR', TRUE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'PRETTY', TRUE);\nEND;\n/\n\nSELECT DBMS_METADATA.GET_DDL(OBJECT_TYPE, OBJECT_NAME, OWNER)\nFROM (\n SELECT 'TABLE' AS OBJECT_TYPE, TABLE_NAME AS OBJECT_NAME, OWNER\n FROM DBA_TABLES\n WHERE OWNER = 'APP_OWNER'\n AND TABLE_NAME NOT LIKE 'BIN$%' -- Exclude recycle bin objects\n AND TABLE_NAME NOT LIKE 'MLOG$_%' -- Exclude MV logs\n AND NESTED = 'NO' -- Exclude nested tables\n ORDER BY TABLE_NAME\n);\n\nSPOOL OFF\n```\n\n### Extracting Grants and Synonyms\n\n```sql\n-- Extract all object grants made by the schema owner\nSELECT DBMS_METADATA.GET_DDL('OBJECT_GRANT', OBJECT_NAME, GRANTOR)\nFROM (\n SELECT DISTINCT OBJECT_NAME, GRANTOR\n FROM DBA_TAB_PRIVS\n WHERE GRANTOR = 'APP_OWNER'\n ORDER BY OBJECT_NAME\n);\n\n-- Extract all public synonyms pointing to APP_OWNER objects\nSELECT DBMS_METADATA.GET_DDL('SYNONYM', SYNONYM_NAME, 'PUBLIC')\nFROM DBA_SYNONYMS\nWHERE TABLE_OWNER = 'APP_OWNER'\n AND OWNER = 'PUBLIC';\n\n-- Extract private synonyms in the schema\nSELECT DBMS_METADATA.GET_DDL('SYNONYM', SYNONYM_NAME, OWNER)\nFROM DBA_SYNONYMS\nWHERE OWNER = 'APP_OWNER';\n```\n\n---\n\n## Comprehensive DDL Export Script\n\nThe following shell script extracts all schema objects into organized files. It is designed to run in CI/CD and on developer workstations.\n\n```shell\n#!/usr/bin/env bash\n# extract_schema.sh — Extract all APP_OWNER objects into organized files\n\nset -euo pipefail\n\nDB_URL=\"${DB_URL:-//localhost:1521/FREEPDB1}\"\nDB_USER=\"${DB_USER:-app_owner}\"\nDB_PASS=\"${DB_PASS:-password}\"\nOUTPUT_DIR=\"${OUTPUT_DIR:-./schema}\"\n\nmkdir -p \"${OUTPUT_DIR}\"/{tables,indexes,views,sequences,packages,procedures,\\\nfunctions,triggers,types,synonyms,grants}\n\n# Function to extract objects of a given type\nextract_objects() {\n local obj_type=\"$1\"\n local out_dir=\"$2\"\n local extension=\"${3:-.sql}\"\n\n sqlplus -S \"${DB_USER}/${DB_PASS}@${DB_URL}\" \u003c\u003cSQL\nSET LONG 999999\nSET PAGESIZE 0\nSET LINESIZE 300\nSET FEEDBACK OFF\nSET HEADING OFF\nSET TRIMSPOOL ON\nSET VERIFY OFF\n\nBEGIN\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'STORAGE', FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'TABLESPACE', FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'SEGMENT_ATTRIBUTES',FALSE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'SQLTERMINATOR', TRUE);\n DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM,'PRETTY', TRUE);\nEND;\n/\n\n-- Write each object to its own file using UTL_FILE would be cleaner,\n-- but this script concatenates to a single file per type for simplicity.\nSPOOL ${out_dir}/all_${obj_type,,}s${extension}\n\nSELECT DBMS_METADATA.GET_DDL(UPPER('${obj_type}'), OBJECT_NAME, OWNER)\n || CHR(10) || '/' || CHR(10)\nFROM USER_OBJECTS\nWHERE OBJECT_TYPE = UPPER('${obj_type}')\n AND OBJECT_NAME NOT LIKE 'SYS\\_%' ESCAPE '\\'\n AND OBJECT_NAME NOT LIKE 'BIN\\$%' ESCAPE '\\'\n AND STATUS = 'VALID'\nORDER BY OBJECT_NAME;\n\nSPOOL OFF\nEXIT\nSQL\n}\n\necho \"Extracting tables...\"\nextract_objects \"TABLE\" \"${OUTPUT_DIR}/tables\"\n\necho \"Extracting indexes...\"\nextract_objects \"INDEX\" \"${OUTPUT_DIR}/indexes\"\n\necho \"Extracting views...\"\nextract_objects \"VIEW\" \"${OUTPUT_DIR}/views\"\n\necho \"Extracting sequences...\"\nextract_objects \"SEQUENCE\" \"${OUTPUT_DIR}/sequences\"\n\necho \"Extracting packages (specs)...\"\nextract_objects \"PACKAGE\" \"${OUTPUT_DIR}/packages\"\n\necho \"Extracting package bodies...\"\nextract_objects \"PACKAGE_BODY\" \"${OUTPUT_DIR}/packages\"\n\necho \"Extracting procedures...\"\nextract_objects \"PROCEDURE\" \"${OUTPUT_DIR}/procedures\"\n\necho \"Extracting functions...\"\nextract_objects \"FUNCTION\" \"${OUTPUT_DIR}/functions\"\n\necho \"Extracting triggers...\"\nextract_objects \"TRIGGER\" \"${OUTPUT_DIR}/triggers\"\n\necho \"Extracting types...\"\nextract_objects \"TYPE\" \"${OUTPUT_DIR}/types\"\n\necho \"Done. Files written to ${OUTPUT_DIR}/\"\n```\n\n---\n\n## Organizing Schema Objects in Git\n\n### Recommended Repository Structure\n\n```\nschema/\n README.md\n install.sql -- Master install script (ordered)\n tables/\n customers.sql\n orders.sql\n order_lines.sql\n products.sql\n indexes/\n idx_orders_customer.sql\n idx_orders_status.sql\n idx_customers_email.sql\n sequences/\n seq_customer_id.sql\n seq_order_id.sql\n views/\n vw_active_customers.sql\n vw_order_summary.sql\n packages/\n pkg_orders.pks -- Package spec (.pks convention)\n pkg_orders.pkb -- Package body (.pkb convention)\n pkg_customers.pks\n pkg_customers.pkb\n procedures/\n prc_archive_old_orders.sql\n functions/\n fnc_calculate_tax.sql\n triggers/\n trg_orders_bi.sql\n trg_customers_audit.sql\n types/\n typ_order_line.sql\n typ_order_line_tbl.sql\n grants/\n grants_to_app_user.sql\n grants_to_report_user.sql\n synonyms/\n public_synonyms.sql\n migrations/\n V001__initial_schema.sql\n V002__add_customer_status.sql\n```\n\n### File Naming Conventions\n\nEstablish and document naming conventions in the repository. Consistency enables tooling:\n\n| Object Type | Convention | Example |\n|---|---|---|\n| Table | `{table_name}.sql` (lowercase) | `orders.sql` |\n| Package spec | `{pkg_name}.pks` | `pkg_orders.pks` |\n| Package body | `{pkg_name}.pkb` | `pkg_orders.pkb` |\n| Index | `{index_name}.sql` | `idx_orders_customer.sql` |\n| Trigger | `{trigger_name}.sql` | `trg_orders_bi.sql` |\n| Type | `{type_name}.sql` | `typ_order_line.sql` |\n\n### Master Install Script\n\nThe `install.sql` script recreates the schema from scratch in the correct dependency order. This is the script used for new environment provisioning and CI database container setup.\n\n```sql\n-- schema/install.sql\n-- Creates all schema objects in dependency order.\n-- Run as DBA or schema owner with CREATE privileges.\n-- Usage: sqlplus user/pass@//host:1521/service @install.sql\n\nWHENEVER SQLERROR EXIT FAILURE ROLLBACK\n\n-- Types (no dependencies)\n@@types/typ_order_line.sql\n@@types/typ_order_line_tbl.sql\n\n-- Sequences (no dependencies)\n@@sequences/seq_customer_id.sql\n@@sequences/seq_order_id.sql\n@@sequences/seq_invoice_id.sql\n\n-- Tables (in FK dependency order)\n@@tables/customers.sql\n@@tables/customer_status_codes.sql\n@@tables/products.sql\n@@tables/product_categories.sql\n@@tables/orders.sql\n@@tables/order_lines.sql\n@@tables/invoices.sql\n\n-- Indexes\n@@indexes/idx_orders_customer.sql\n@@indexes/idx_orders_status.sql\n@@indexes/idx_customers_email.sql\n@@indexes/idx_order_lines_order.sql\n\n-- Views (depend on tables)\n@@views/vw_active_customers.sql\n@@views/vw_order_summary.sql\n@@views/vw_invoice_detail.sql\n\n-- Package specs (can depend on types, sequences)\n@@packages/pkg_customers.pks\n@@packages/pkg_orders.pks\n@@packages/pkg_invoicing.pks\n\n-- Package bodies (depend on specs)\n@@packages/pkg_customers.pkb\n@@packages/pkg_orders.pkb\n@@packages/pkg_invoicing.pkb\n\n-- Standalone procedures and functions\n@@procedures/prc_archive_old_orders.sql\n@@functions/fnc_calculate_tax.sql\n\n-- Triggers (depend on tables and sometimes packages)\n@@triggers/trg_customers_bi.sql\n@@triggers/trg_orders_bi.sql\n@@triggers/trg_orders_audit.sql\n\n-- Grants (depend on all objects existing)\n@@grants/grants_to_app_user.sql\n@@grants/grants_to_report_user.sql\n\n-- Synonyms (depend on grants)\n@@synonyms/public_synonyms.sql\n\nPROMPT Schema installation complete.\n```\n\n---\n\n## SQL Developer Source Control Integration\n\nOracle SQL Developer has native integration with Git (and SVN). The integration supports:\n\n- Browsing repository history for any object\n- Comparing current database state to repository state\n- Checking out repository versions directly into the database\n- Committing DDL extracts directly to git\n\n### Connecting a Repository\n\n1. In SQL Developer: **Team** > **Git** > **Clone**\n2. Enter repository URL, credentials, and local path\n3. SQL Developer maintains a local working copy\n\n### Configuring Object Export for Version Control\n\n**Tools** > **Preferences** > **Database** > **Object Viewer** > **DDL**:\n\n- Uncheck \"Include Schema\": prevents `APP_OWNER.ORDERS` becoming `ORDERS` (schema-portable DDL is cleaner)\n- Check \"Include Terminator\": ensures each file ends with `;` or `/`\n- Check \"Pretty Print\"\n\n### SQL Developer Migration Repository\n\nFor team environments, SQL Developer's migration repository captures the database state at a point in time and can diff it against a target. Set this up under **Tools** > **Migration** > **Create Repository**.\n\n### Using the DB Differ\n\nSQL Developer's **Database Diff** tool (under **Tools** > **Diff**) compares two schemas and generates a synchronization script. This is useful for:\n\n- Comparing DEV schema to PROD to find unauthorized manual changes\n- Generating the change script for a release\n- Verifying that a migration was applied correctly\n\n```shell\n# Command-line equivalent using SQLcl (Oracle's modern SQL*Plus)\nsql -S app_owner/password@//host:1521/service \u003c\u003c'EOF'\n -- SQLcl DDL export for a table (clean, formatted)\n DDL ORDERS\n\n -- Export entire schema\n DDL APP_OWNER.*\nEOF\n```\n\n---\n\n## Handling Grants in Version Control\n\n### Grant File Structure\n\nGrants should be stored idempotently — they can be re-run without error even if the grant already exists. `GRANT` statements are idempotent in Oracle (re-granting an already-granted privilege is a no-op, not an error).\n\n```sql\n-- schema/grants/grants_to_app_user.sql\n-- Grants to APP_USER (runtime application user)\n-- Run as APP_OWNER\n\n-- Table grants\nGRANT SELECT, INSERT, UPDATE, DELETE ON ORDERS TO APP_USER;\nGRANT SELECT, INSERT, UPDATE, DELETE ON ORDER_LINES TO APP_USER;\nGRANT SELECT, INSERT, UPDATE, DELETE ON CUSTOMERS_T TO APP_USER;\nGRANT SELECT ON CUSTOMER_STATUS_CODES TO APP_USER;\nGRANT SELECT ON PRODUCTS TO APP_USER;\n\n-- Sequence grants\nGRANT SELECT ON SEQ_ORDER_ID TO APP_USER;\nGRANT SELECT ON SEQ_CUSTOMER_ID TO APP_USER;\n\n-- View grants\nGRANT SELECT ON VW_ACTIVE_CUSTOMERS TO APP_USER;\nGRANT SELECT ON VW_ORDER_SUMMARY TO APP_USER;\n\n-- Package execute grants\nGRANT EXECUTE ON PKG_ORDERS TO APP_USER;\nGRANT EXECUTE ON PKG_CUSTOMERS TO APP_USER;\nGRANT EXECUTE ON PKG_INVOICING TO APP_USER;\n```\n\n```sql\n-- schema/grants/grants_to_report_user.sql\n-- Read-only grants for reporting user REPORT_USER\n\nGRANT SELECT ON ORDERS TO REPORT_USER;\nGRANT SELECT ON ORDER_LINES TO REPORT_USER;\nGRANT SELECT ON CUSTOMERS_T TO REPORT_USER;\nGRANT SELECT ON PRODUCTS TO REPORT_USER;\nGRANT SELECT ON VW_ORDER_SUMMARY TO REPORT_USER;\n```\n\n### Discovering Undocumented Grants\n\nRegularly compare the grants in the database to the grants in the repository to detect drift:\n\n```sql\n-- Find grants in the database not represented in version control\n-- (Run this query and compare output to your grants file)\nSELECT\n GRANTEE,\n PRIVILEGE,\n OWNER AS OBJECT_OWNER,\n TABLE_NAME AS OBJECT_NAME,\n GRANTABLE,\n HIERARCHY\nFROM\n DBA_TAB_PRIVS\nWHERE\n GRANTOR = 'APP_OWNER'\nORDER BY\n GRANTEE, OBJECT_NAME, PRIVILEGE;\n```\n\n---\n\n## Handling Synonyms in Version Control\n\n### Public Synonyms\n\n```sql\n-- schema/synonyms/public_synonyms.sql\n-- Run as SYS or a DBA with CREATE PUBLIC SYNONYM privilege\n\n-- Create or replace public synonyms pointing to APP_OWNER\nCREATE OR REPLACE PUBLIC SYNONYM ORDERS FOR APP_OWNER.ORDERS;\nCREATE OR REPLACE PUBLIC SYNONYM ORDER_LINES FOR APP_OWNER.ORDER_LINES;\nCREATE OR REPLACE PUBLIC SYNONYM CUSTOMERS FOR APP_OWNER.CUSTOMERS;\nCREATE OR REPLACE PUBLIC SYNONYM VW_ORDER_SUMMARY FOR APP_OWNER.VW_ORDER_SUMMARY;\nCREATE OR REPLACE PUBLIC SYNONYM PKG_ORDERS FOR APP_OWNER.PKG_ORDERS;\nCREATE OR REPLACE PUBLIC SYNONYM PKG_CUSTOMERS FOR APP_OWNER.PKG_CUSTOMERS;\n```\n\n### Detecting Synonym Drift\n\n```sql\n-- Find public synonyms pointing to APP_OWNER objects\n-- that are not in the synonyms file\nSELECT\n SYNONYM_NAME,\n TABLE_OWNER,\n TABLE_NAME,\n DB_LINK\nFROM\n DBA_SYNONYMS\nWHERE\n OWNER = 'PUBLIC'\n AND TABLE_OWNER = 'APP_OWNER'\nORDER BY\n SYNONYM_NAME;\n```\n\n---\n\n## Detecting and Resolving Schema Drift\n\nSchema drift occurs when the database is modified directly without updating the repository. Regular drift detection prevents environments from diverging silently.\n\n```sql\n-- Compare object checksums/timestamps between two environments\n-- Run in a pipeline comparing DEV to the last known-good state\n\n-- Objects modified more recently than last deployment\nSELECT\n OBJECT_NAME,\n OBJECT_TYPE,\n STATUS,\n LAST_DDL_TIME,\n CREATED\nFROM\n DBA_OBJECTS\nWHERE\n OWNER = 'APP_OWNER'\n AND OBJECT_TYPE IN ('TABLE','VIEW','PACKAGE','PACKAGE BODY',\n 'PROCEDURE','FUNCTION','TRIGGER','INDEX')\n AND LAST_DDL_TIME > TO_DATE('2025-01-01','YYYY-MM-DD') -- Replace with last deployment time\nORDER BY\n LAST_DDL_TIME DESC;\n```\n\n```shell\n# Pipeline drift detection: extract current DDL and diff against git\n./extract_schema.sh # Writes to ./schema/\n\ngit diff --stat schema/\n# If output is non-empty, the database has drifted from the repository\n```\n\n---\n\n## Best Practices\n\n- **One object per file.** Having `CREATE TABLE ORDERS` and `CREATE INDEX IDX_ORDERS_CUSTOMER` in the same file makes it impossible to diff or review changes to just the index. One file per object is the only scalable approach.\n- **Store PL/SQL package spec and body in separate files.** Specs change much less frequently than bodies. Keeping them separate reduces diff noise and allows partial deployments (body-only redeploy without recompiling dependents).\n- **Never commit the result of `DBMS_METADATA.GET_DDL` without transforming it first.** Raw output includes storage clauses, segment attributes, and physical parameters that are environment-specific. Always apply the transform parameters that strip physical attributes before committing.\n- **Validate DDL files on every pull request.** Use SQLcl or SQL*Plus in `WHENEVER SQLERROR EXIT` mode to syntax-check DDL files in CI, even against an empty schema. Syntax errors in repository files are caught before they reach any real environment.\n- **Track `DATABASECHANGELOG` separately.** If using Liquibase or Flyway, the migration history table is not a schema object to be reverse-engineered. It is managed by the migration tool.\n- **Automate drift detection.** Run a weekly job that extracts the production schema DDL, commits it to a `schema-snapshot` branch, and raises an alert if it differs from the main `schema` branch. Any diff reveals unauthorized manual changes.\n- **Document deployment order in `install.sql`.** The install script serves as executable documentation of object dependencies. Keep it current.\n\n---\n\n## Common Mistakes\n\n**Mistake: Committing DDL with storage clauses.**\nStorage clause differences between environments (different tablespace names, extent sizes) make every file appear different on every extraction. Always strip physical attributes using `DBMS_METADATA.SET_TRANSFORM_PARAM`.\n\n**Mistake: Storing view and package source in migration scripts only.**\nIf the only copy of a view's definition is in `V15__update_order_view.sql`, reconstructing the current view definition requires running all 15 migrations in order. Store the current, canonical definition as a separate file in `views/` and keep the migration for the historical change record.\n\n**Mistake: Forgetting grants and synonyms entirely.**\nGrants and synonyms are frequently omitted from DDL repositories. When a new environment is provisioned from `install.sql` without them, the application fails at runtime with privilege errors. Include grants and synonyms as explicit files in the repository.\n\n**Mistake: Using schema-qualified object names in DDL files.**\nDDL files that contain `CREATE TABLE APP_OWNER.ORDERS` are tied to a specific schema name. This breaks when deploying to a schema with a different name (common in multi-tenant setups or personal developer schemas). Use unqualified names and rely on the deployment connection's schema context.\n\n**Mistake: Treating the repository as append-only for PL/SQL.**\nSome teams only add new migration files and never update the standalone package/procedure files, letting them fall out of sync with the database. Establish a process: when a package changes via migration, also update the corresponding `.pks` / `.pkb` file in the same commit so the repository always reflects the current state.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [DBMS_METADATA (19c)](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_METADATA.html) — GET_DDL, SET_TRANSFORM_PARAM, SESSION_TRANSFORM, supported object types\n- [Oracle Database Reference 19c — DBA_OBJECTS](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/DBA_OBJECTS.html) — LAST_DDL_TIME for drift detection\n- [Oracle Database Reference 19c — DBA_TAB_PRIVS](https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/DBA_TAB_PRIVS.html) — grant tracking\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21407,"content_sha256":"7463d35fb3be296348a082a2bf9dc4bd940e8b19c9b51aad820b3593a888c6c4"},{"filename":"features/advanced-queuing.md","content":"# Oracle Advanced Queuing (AQ) and Transactional Event Queues (TQ)\n\n## Overview\n\nOracle Advanced Queuing (AQ) is a database-integrated message queuing facility built directly into the Oracle database engine. Unlike external messaging systems, AQ stores messages in ordinary database tables, which means messages participate fully in Oracle transactions, benefit from the database's reliability guarantees, and are queryable using standard SQL.\n\nOracle Database 21c rebranded and significantly enhanced AQ as **Transactional Event Queues (TQ/TEQ)**, adding high-throughput partitioned storage, Kafka-compatible APIs, and improved scalability while maintaining full backward compatibility with the original AQ APIs.\n\n**When to use Oracle AQ/TQ:**\n- Applications already running on Oracle that need guaranteed message delivery\n- Scenarios requiring transactional consistency between database writes and message publication\n- Systems where message data needs to be queryable or reportable\n- Environments where adding an external broker (Kafka, RabbitMQ) adds unwanted operational complexity\n\n---\n\n## Core Concepts\n\n### Queue Types\n\n**Single-Consumer Queues**\nA message is dequeued by exactly one consumer. This is the simplest model and maps directly to a point-to-point (P2P) messaging pattern.\n\n**Multi-Consumer Queues**\nMultiple named subscribers can each receive a copy of every message. This maps to a publish/subscribe (pub/sub) pattern. Each subscriber maintains its own logical position in the queue.\n\n**Exception Queues**\nEvery queue is associated with an exception queue. Messages that cannot be delivered (e.g., maximum retry count exceeded) are moved here automatically, preventing queue poisoning.\n\n### Message Payload Types\n\n| Payload Type | Description |\n|---|---|\n| `RAW` | Unstructured binary data |\n| `VARCHAR2` | Character string payload |\n| Object type | Any Oracle object type (most common) |\n| `DBMS_AQ.AQ$_JMS_TEXT_MESSAGE` | JMS-compatible text message |\n| `DBMS_AQ.AQ$_JMS_MAP_MESSAGE` | JMS-compatible map message |\n| `SYS.ANYDATA` | Self-describing type for heterogeneous payloads |\n| `JSON` | Native JSON payload (21c+) |\n\n### Queue Tables vs Queues\n\nA **queue table** is the underlying database table that stores messages. A **queue** is a logical object layered on top of a queue table. One queue table can host multiple queues. Queue tables have specific storage options and index structures managed by `DBMS_AQADM`.\n\n---\n\n## DBMS_AQADM — Administration Package\n\n`DBMS_AQADM` handles the lifecycle of queue infrastructure: creating queue tables, creating queues, starting and stopping queues, and adding/removing subscribers.\n\n### Creating a Queue Table\n\n```sql\n-- Define a payload type first\nCREATE OR REPLACE TYPE order_payload_t AS OBJECT (\n order_id NUMBER,\n customer_id NUMBER,\n status VARCHAR2(50),\n total_amount NUMBER(10,2),\n created_at TIMESTAMP\n);\n/\n\n-- Create a multi-consumer queue table using the object type\nBEGIN\n DBMS_AQADM.CREATE_QUEUE_TABLE(\n queue_table => 'order_queue_tab',\n queue_payload_type => 'order_payload_t',\n multiple_consumers => TRUE, -- FALSE for single-consumer\n sort_list => 'PRIORITY,ENQ_TIME', -- dequeue ordering\n comment => 'Order processing event queue'\n );\nEND;\n/\n```\n\n### Creating and Starting a Queue\n\n```sql\nBEGIN\n -- Create the queue on top of the queue table\n DBMS_AQADM.CREATE_QUEUE(\n queue_name => 'order_events_q',\n queue_table => 'order_queue_tab',\n queue_type => DBMS_AQADM.NORMAL_QUEUE,\n max_retries => 5,\n retry_delay => 60, -- seconds before retry after rollback\n retention_time => 86400, -- seconds to keep dequeued messages (0 = purge immediately)\n comment => 'Order lifecycle events'\n );\n\n -- Start the queue (enable both enqueue and dequeue)\n DBMS_AQADM.START_QUEUE(\n queue_name => 'order_events_q',\n enqueue => TRUE,\n dequeue => TRUE\n );\nEND;\n/\n```\n\n### Adding Subscribers (Multi-Consumer)\n\n```sql\nDECLARE\n subscriber_agent SYS.AQ$_AGENT;\nBEGIN\n -- Subscriber for the fulfillment service\n subscriber_agent := SYS.AQ$_AGENT(\n name => 'FULFILLMENT_SERVICE',\n address => NULL,\n protocol => 0\n );\n\n DBMS_AQADM.ADD_SUBSCRIBER(\n queue_name => 'order_events_q',\n subscriber => subscriber_agent,\n rule => 'tab.user_data.status = ''NEW''' -- content-based filtering\n );\n\n -- Subscriber for the analytics pipeline (no filter — receives all messages)\n DBMS_AQADM.ADD_SUBSCRIBER(\n queue_name => 'order_events_q',\n subscriber => SYS.AQ$_AGENT('ANALYTICS_SERVICE', NULL, 0)\n );\nEND;\n/\n```\n\n---\n\n## DBMS_AQ — Enqueue and Dequeue Package\n\n### Enqueue Options\n\n```sql\nDECLARE\n enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T;\n message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;\n message_handle RAW(16);\n payload order_payload_t;\nBEGIN\n -- Build the payload\n payload := order_payload_t(\n order_id => 10042,\n customer_id => 5001,\n status => 'NEW',\n total_amount => 249.99,\n created_at => SYSTIMESTAMP\n );\n\n -- Configure enqueue options\n enqueue_options.visibility := DBMS_AQ.ON_COMMIT; -- message visible after COMMIT\n -- DBMS_AQ.IMMEDIATE makes message visible before COMMIT (use carefully)\n\n -- Configure message properties\n message_properties.priority := 1; -- lower number = higher priority\n message_properties.delay := 0; -- delay in seconds before message is available\n message_properties.expiration := 3600; -- expire after 1 hour if not dequeued\n message_properties.correlation := 'order-10042'; -- application-level correlation ID\n\n DBMS_AQ.ENQUEUE(\n queue_name => 'order_events_q',\n enqueue_options => enqueue_options,\n message_properties => message_properties,\n payload => payload,\n msgid => message_handle\n );\n\n COMMIT;\n DBMS_OUTPUT.PUT_LINE('Enqueued message ID: ' || RAWTOHEX(message_handle));\nEND;\n/\n```\n\n### Dequeue Options\n\n```sql\nDECLARE\n dequeue_options DBMS_AQ.DEQUEUE_OPTIONS_T;\n message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;\n message_handle RAW(16);\n payload order_payload_t;\nBEGIN\n -- Configure dequeue options\n dequeue_options.consumer_name := 'FULFILLMENT_SERVICE'; -- required for multi-consumer\n dequeue_options.dequeue_mode := DBMS_AQ.REMOVE; -- REMOVE, BROWSE, LOCKED\n dequeue_options.navigation := DBMS_AQ.NEXT_MESSAGE;\n dequeue_options.visibility := DBMS_AQ.ON_COMMIT;\n dequeue_options.wait := 30; -- wait up to 30 seconds; DBMS_AQ.NO_WAIT or DBMS_AQ.FOREVER\n\n -- Optional: filter by correlation or message ID\n -- dequeue_options.correlation := 'order-10042';\n\n DBMS_AQ.DEQUEUE(\n queue_name => 'order_events_q',\n dequeue_options => dequeue_options,\n message_properties => message_properties,\n payload => payload,\n msgid => message_handle\n );\n\n DBMS_OUTPUT.PUT_LINE('Processing order: ' || payload.order_id);\n -- ... business logic ...\n\n COMMIT; -- message is permanently removed from queue on commit\nEXCEPTION\n WHEN DBMS_AQ.TIME_OUT THEN\n DBMS_OUTPUT.PUT_LINE('No messages available within wait period');\n WHEN DBMS_AQ.NO_MESSAGE_FOUND THEN\n DBMS_OUTPUT.PUT_LINE('Queue is empty');\nEND;\n/\n```\n\n### Browse Mode (Non-Destructive Read)\n\n```sql\nDECLARE\n dequeue_options DBMS_AQ.DEQUEUE_OPTIONS_T;\n message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;\n message_handle RAW(16);\n payload order_payload_t;\nBEGIN\n dequeue_options.dequeue_mode := DBMS_AQ.BROWSE;\n dequeue_options.navigation := DBMS_AQ.FIRST_MESSAGE;\n dequeue_options.wait := DBMS_AQ.NO_WAIT;\n\n LOOP\n BEGIN\n DBMS_AQ.DEQUEUE(\n queue_name => 'order_events_q',\n dequeue_options => dequeue_options,\n message_properties => message_properties,\n payload => payload,\n msgid => message_handle\n );\n DBMS_OUTPUT.PUT_LINE('Found: order_id=' || payload.order_id\n || ' priority=' || message_properties.priority);\n dequeue_options.navigation := DBMS_AQ.NEXT_MESSAGE;\n EXCEPTION\n WHEN DBMS_AQ.NO_MESSAGE_FOUND THEN EXIT;\n END;\n END LOOP;\nEND;\n/\n```\n\n---\n\n## Message Propagation\n\nOracle AQ supports automatic **propagation** of messages between queues, including queues in remote databases via database links. This enables distributed messaging without application-level forwarding code.\n\n```sql\n-- Enable propagation from local queue to a remote queue on db_link REMOTE_DB\nBEGIN\n DBMS_AQADM.SCHEDULE_PROPAGATION(\n queue_name => 'order_events_q',\n destination => 'REMOTE_DB', -- database link name\n start_time => SYSDATE,\n duration => NULL, -- NULL = propagate indefinitely\n next_time => NULL, -- NULL = continuous propagation\n latency => 5, -- max seconds of message latency\n destination_queue => 'remote_orders_q' -- target queue name on remote DB\n );\nEND;\n/\n\n-- Check propagation schedules\nSELECT queue_name, destination, schedule_disabled, failures\nFROM dba_queue_schedules;\n```\n\n---\n\n## JMS Integration\n\nOracle AQ implements the Java Message Service (JMS) specification through the Oracle AQ JMS provider. This allows Java EE applications to use standard JMS APIs backed by Oracle database queues.\n\n```sql\n-- Create a JMS-compatible queue using the built-in JMS payload type\nBEGIN\n DBMS_AQADM.CREATE_QUEUE_TABLE(\n queue_table => 'jms_text_queue_tab',\n queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE',\n multiple_consumers => TRUE\n );\n\n DBMS_AQADM.CREATE_QUEUE(\n queue_name => 'jms_app_queue',\n queue_table => 'jms_text_queue_tab'\n );\n\n DBMS_AQADM.START_QUEUE(queue_name => 'jms_app_queue');\nEND;\n/\n```\n\nJava-side JNDI/JMS connection to Oracle AQ uses `oracle.jms.AQjmsFactory` with a standard JDBC data source. The queue appears as a standard JMS `Queue` or `Topic`.\n\n---\n\n## Transactional Event Queues (21c+)\n\nTEQ enhances AQ with a **partitioned, high-throughput storage engine** designed for event streaming workloads.\n\n```sql\n-- Create a Transactional Event Queue (21c+)\nBEGIN\n DBMS_AQADM.CREATE_TRANSACTIONAL_EVENT_QUEUE(\n queue_name => 'iot_sensor_teq',\n queue_payload_type => 'JSON',\n multiple_consumers => TRUE\n );\n\n DBMS_AQADM.START_QUEUE(queue_name => 'iot_sensor_teq');\nEND;\n/\n\n-- Kafka-compatible producer (21c+ with kafka_aq_adapter)\n-- Oracle provides a Kafka adapter that lets Kafka clients connect to TEQ\n-- without code changes, treating TEQ as a Kafka topic.\n```\n\n---\n\n## Monitoring and Administration\n\n```sql\n-- View all queues and their status\nSELECT owner, name, queue_type, enqueue_enabled, dequeue_enabled, queue_table\nFROM dba_queues\nORDER BY owner, name;\n\n-- Count messages in each queue by state\nSELECT q.name AS queue_name,\n t.msg_state,\n COUNT(*) AS message_count,\n MIN(t.enq_time) AS oldest_message\nFROM dba_queues q\nJOIN aq$order_queue_tab t ON t.q_name = q.name\nGROUP BY q.name, t.msg_state;\n\n-- Messages in exception (dead-letter) queue\nSELECT msgid, enq_time, exception_queue,\n user_data.order_id, user_data.status\nFROM aq$order_queue_tab\nWHERE msg_state = 'EXPIRED';\n\n-- View subscriber information\nSELECT queue_name, consumer_name, address, rule\nFROM dba_queue_subscribers;\n\n-- Current propagation health\nSELECT queue_name, destination, last_run_time, next_run_time, failures, last_error_msg\nFROM dba_queue_schedules;\n```\n\n---\n\n## AQ vs Apache Kafka — Comparison\n\n| Dimension | Oracle AQ / TEQ | Apache Kafka |\n|---|---|---|\n| Transactional consistency | Native, same ACID transaction as DB writes | Requires transactional producers (additional config) |\n| Message queryability | Full SQL access to queue tables | Requires external tooling (ksqlDB, etc.) |\n| Operational footprint | Zero — part of Oracle DB | Separate cluster (ZooKeeper/KRaft + brokers) |\n| Throughput (raw) | Moderate (TEQ improves significantly) | Very high — designed for streaming at scale |\n| Message retention | Configurable; tied to DB storage | Log-based; designed for long retention |\n| Schema enforcement | Oracle object types or JSON schema | Schema Registry (optional) |\n| Ecosystem | Oracle-centric; JMS compatible | Huge ecosystem (Kafka Connect, Streams, etc.) |\n| Replay / rewind | Limited (retention_time window) | First-class feature (offset reset) |\n| Best for | DB-integrated transactional messaging | High-throughput event streaming pipelines |\n\n---\n\n## Best Practices\n\n- **Use `ON_COMMIT` visibility** for both enqueue and dequeue in OLTP systems. `IMMEDIATE` visibility can expose messages before the producing transaction completes, leading to consumers seeing partial data.\n- **Always handle `DBMS_AQ.TIME_OUT` and `DBMS_AQ.NO_MESSAGE_FOUND`** exceptions in dequeue loops. A missing handler causes silent consumer failures.\n- **Set `max_retries` and `retry_delay`** on every queue. Without `max_retries`, a persistently failing message will block processing indefinitely. With it, messages flow to the exception queue after the limit.\n- **Separate queue tables by payload type and consumer pattern.** Mixing single-consumer and multi-consumer queues in the same table is not allowed and mixing unrelated payload types makes monitoring confusing.\n- **Monitor exception queues regularly.** Build an alert or scheduled job that reports when any message appears in an exception queue.\n- **Use content-based routing rules on subscribers** rather than creating separate queues per use case. This reduces queue table proliferation.\n- **Grant only necessary privileges.** Use `DBMS_AQADM.GRANT_QUEUE_PRIVILEGE` rather than granting direct table access to the underlying queue table.\n- **Purge old messages.** Use `DBMS_AQADM.PURGE_QUEUE_TABLE` with a purge condition to remove processed messages and control table growth.\n\n```sql\n-- Scheduled purge of processed (dequeued) messages older than 7 days\nDECLARE\n purge_options DBMS_AQADM.AQ$_PURGE_OPTIONS_T;\nBEGIN\n purge_options.block := FALSE;\n DBMS_AQADM.PURGE_QUEUE_TABLE(\n queue_table => 'order_queue_tab',\n purge_condition => 'qtview.msg_state = ''PROCESSED'' AND qtview.enq_time \u003c SYSDATE - 7',\n purge_options => purge_options\n );\nEND;\n/\n```\n\n---\n\n## Common Mistakes and How to Avoid Them\n\n**Mistake 1: Committing after enqueue but forgetting the dequeue commit**\nIf a dequeue uses `ON_COMMIT` visibility and the session rolls back instead of committing, the message is returned to the queue. This is intentional and correct behavior — do not fight it. Structure dequeue code so that business logic and the final COMMIT are atomic.\n\n**Mistake 2: Creating too many queue tables**\nEach queue table generates several internal Oracle objects (IOTs, indexes, etc.). Creating dozens of queue tables for minor variations wastes resources. Use subscriber rules and correlation IDs to differentiate message streams within a single queue.\n\n**Mistake 3: Using `DBMS_AQ.FOREVER` wait in high-volume applications**\nA session blocking forever with `FOREVER` wait consumes a database connection indefinitely. In connection-pool environments this starves other users. Use a finite wait (e.g., 30–60 seconds) and loop.\n\n**Mistake 4: Not testing exception queue behavior**\nTeams set `max_retries` but never test what happens when it is exceeded. Simulate a failure scenario during development to confirm messages land in the exception queue and that your monitoring alert fires.\n\n**Mistake 5: Dropping a queue without stopping it first**\nAlways call `DBMS_AQADM.STOP_QUEUE` before `DBMS_AQADM.DROP_QUEUE`, and drop queues before dropping the queue table. Skipping steps causes ORA-24005 and related errors.\n\n```sql\n-- Correct teardown sequence\nBEGIN\n DBMS_AQADM.STOP_QUEUE(queue_name => 'order_events_q');\n DBMS_AQADM.DROP_QUEUE(queue_name => 'order_events_q');\n DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'order_queue_tab');\nEND;\n/\n```\n\n**Mistake 6: Ignoring propagation failures**\nPropagation schedules silently accumulate failure counts when the remote database or network is unavailable. Set up a job that queries `DBA_QUEUE_SCHEDULES.FAILURES > 0` and alerts the operations team.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [DBMS_AQADM — Oracle Database PL/SQL Packages and Types Reference 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_AQADM.html)\n- [DBMS_AQ — Oracle Database PL/SQL Packages and Types Reference 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_AQ.html)\n- [Oracle Database Advanced Queuing User's Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/adque/index.html)\n- [Transactional Event Queues and Advanced Queuing — Oracle 21c](https://docs.oracle.com/en/database/oracle/oracle-database/21/adque/index.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":17997,"content_sha256":"c81c64e49cf345e49e5cb26547ad2993a708dbe4c0020e3ab8b0d0d22bca4361"},{"filename":"features/ai-profiles.md","content":"# AI Profiles and Provider Configuration in Oracle 26ai\n\nAI profiles connect Oracle Database to external LLM providers for `DBMS_CLOUD_AI` and SELECT AI workflows.\n\nThis guide targets the Oracle 26ai AI profile APIs and provider attributes. `DBMS_VECTOR` and `DBMS_VECTOR_CHAIN` use the same providers, but they do not use `DBMS_CLOUD_AI` profiles directly; they use vector credentials and per-call JSON parameters.\n\n## Architecture Overview\n\n```\nSELECT AI / DBMS_CLOUD_AI\n ↓\nAI Profile (provider + credential + model + objects)\n ↓\nDBMS_CLOUD Credential (API key, stored encrypted)\n ↓\nExternal LLM Provider\n\nDBMS_VECTOR / DBMS_VECTOR_CHAIN\n ↓\nVector credential + per-call JSON params\n ↓\nExternal embedding / generation provider\n```\n\n## Step 1: Create Provider Credentials\n\nCredentials store API keys encrypted in the Oracle credential store.\n\n```sql\n-- OpenAI\nBEGIN\n DBMS_CLOUD.CREATE_CREDENTIAL(\n credential_name => 'OPENAI_CRED',\n username => 'OPENAI',\n password => 'sk-proj-...' -- your OpenAI API key\n );\nEND;\n/\n\n-- Cohere\nBEGIN\n DBMS_CLOUD.CREATE_CREDENTIAL(\n credential_name => 'COHERE_CRED',\n username => 'COHERE',\n password => 'co-...' -- your Cohere API key\n );\nEND;\n/\n\n-- Azure OpenAI\nBEGIN\n DBMS_CLOUD.CREATE_CREDENTIAL(\n credential_name => 'AZURE_CRED',\n username => 'AZURE_OPENAI',\n password => '...' -- your Azure OpenAI key\n );\nEND;\n/\n\n-- Anthropic\nBEGIN\n DBMS_CLOUD.CREATE_CREDENTIAL(\n credential_name => 'ANTHROPIC_CRED',\n username => 'ANTHROPIC',\n password => 'sk-ant-...' -- your Anthropic API key\n );\nEND;\n/\n```\n\n### OCI Generative AI on OCI\n\nWhen the database is configured for OCI resource principal access, use the reserved credential name `OCI$RESOURCE_PRINCIPAL` in the profile. You do not create that credential with `DBMS_CLOUD.CREATE_CREDENTIAL`.\n\n## Step 2: Create an AI Profile\n\n### OpenAI Profile\n\n```sql\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'OPENAI_PROFILE',\n attributes => '{\n \"provider\": \"openai\",\n \"credential_name\": \"OPENAI_CRED\",\n \"object_list\": [{\"owner\": \"HR\", \"name\": \"EMPLOYEES\"},\n {\"owner\": \"HR\", \"name\": \"DEPARTMENTS\"},\n {\"owner\": \"SALES\", \"name\": \"ORDERS\"}],\n \"model\": \"gpt-4o\",\n \"temperature\": 0,\n \"max_tokens\": 2048,\n \"comments\": true\n }'\n );\nEND;\n/\n```\n\n### Cohere Profile\n\n```sql\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'COHERE_PROFILE',\n attributes => '{\n \"provider\": \"cohere\",\n \"credential_name\": \"COHERE_CRED\",\n \"object_list\": [{\"owner\": \"SALES\"}],\n \"model\": \"command-r-plus\",\n \"temperature\": 0\n }'\n );\nEND;\n/\n```\n\n### Azure OpenAI Profile\n\n```sql\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'AZURE_PROFILE',\n attributes => '{\n \"provider\": \"azure\",\n \"credential_name\": \"AZURE_CRED\",\n \"azure_resource_name\": \"my-azure-resource\",\n \"azure_deployment_name\": \"gpt-4o-deployment\",\n \"object_list\": [{\"owner\": \"FINANCE\"}],\n \"model\": \"gpt-4o\",\n \"temperature\": 0\n }'\n );\nEND;\n/\n```\n\n### OCI Generative AI Profile\n\n```sql\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'OCI_GENAI_PROFILE',\n attributes => '{\n \"provider\": \"oci\",\n \"credential_name\": \"OCI$RESOURCE_PRINCIPAL\",\n \"region\": \"us-chicago-1\",\n \"object_list\": [{\"owner\": \"MYAPP\"}],\n \"model\": \"cohere.command-r-plus\",\n \"oci_apiformat\": \"COHERE\"\n }'\n );\nEND;\n/\n```\n\n## Profile Attributes Reference\n\n| Attribute | Required | Description |\n|---|---|---|\n| `provider` | Yes | Provider name: `openai`, `cohere`, `azure`, `oci`, `google`, `anthropic`, `huggingface`, `aws`, or `database` |\n| `credential_name` | Yes | Name of DBMS_CLOUD credential |\n| `object_list` | No (but strongly recommended for SELECT AI) | Tables/views the LLM may reference. Use `[{\"owner\":\"SCHEMA\"}]` for all objects in a schema |\n| `model` | No | Model name; uses provider default if omitted |\n| `temperature` | No | Randomness 0.0–2.0; `0` = deterministic (recommended for SQL generation) |\n| `max_tokens` | No | Maximum tokens in LLM response |\n| `comments` | No | `true` to include table/column comments as schema context |\n| `azure_resource_name` | Azure only | Azure OpenAI resource name |\n| `azure_deployment_name` | Azure only | Azure deployment name |\n| `region` | OCI only | OCI region such as `us-chicago-1` |\n| `provider_endpoint` | Optional | Override the default provider endpoint |\n| `oci_apiformat` | OCI only | `COHERE` or `GENERIC` depending on model |\n\n## Managing Profiles\n\n```sql\n-- List profile metadata in the current schema\nSELECT profile_name, status, description\nFROM user_cloud_ai_profiles;\n\n-- Inspect attributes for one profile\nSELECT profile_name, attribute_name, attribute_value\nFROM user_cloud_ai_profile_attributes\nWHERE profile_name = 'OPENAI_PROFILE'\nORDER BY attribute_name;\n\n-- Set active profile for current session\nEXEC DBMS_CLOUD_AI.SET_PROFILE('OPENAI_PROFILE');\n\n-- Check current session profile\nSELECT DBMS_CLOUD_AI.GET_PROFILE() FROM DUAL;\n\n-- Update a profile attribute\nBEGIN\n DBMS_CLOUD_AI.SET_ATTRIBUTE(\n profile_name => 'OPENAI_PROFILE',\n attribute_name => 'temperature',\n attribute_value => '0'\n );\nEND;\n/\n\n-- Disable and re-enable a profile\nEXEC DBMS_CLOUD_AI.DISABLE_PROFILE('OPENAI_PROFILE');\nEXEC DBMS_CLOUD_AI.ENABLE_PROFILE('OPENAI_PROFILE');\n\n-- Drop a profile\nBEGIN\n DBMS_CLOUD_AI.DROP_PROFILE(profile_name => 'OPENAI_PROFILE');\nEND;\n/\n```\n\n## Vector Package Credentials and Parameters\n\n`DBMS_VECTOR` and `DBMS_VECTOR_CHAIN` do not use `DBMS_CLOUD_AI` profiles. Create a vector credential, then reference it from the JSON `params` passed to vector package calls.\n\n```sql\n-- Create a credential for DBMS_VECTOR / DBMS_VECTOR_CHAIN\nBEGIN\n DBMS_VECTOR.CREATE_CREDENTIAL(\n credential_name => 'OPENAI_VEC_CRED',\n params => JSON_OBJECT(\n 'username' VALUE 'OPENAI',\n 'password' VALUE 'sk-proj-...'\n )\n );\nEND;\n/\n\n-- Use the vector credential in an embedding call\nDECLARE\n v_embed VECTOR;\nBEGIN\n v_embed := DBMS_VECTOR.UTL_TO_EMBEDDING(\n data => :text,\n params => JSON_OBJECT(\n 'provider' VALUE 'openai',\n 'credential_name' VALUE USER || '.OPENAI_VEC_CRED',\n 'model' VALUE 'text-embedding-3-small'\n )\n );\nEND;\n/\n```\n\n## Testing a Profile After Creation\n\nVerify end-to-end connectivity immediately after creating a profile before relying on it in application code.\n\n```sql\n-- Test that a profile is working end-to-end\nDECLARE\n v_result CLOB;\nBEGIN\n v_result := DBMS_CLOUD_AI.GENERATE(\n prompt => 'Say the word CONNECTED in capital letters and nothing else.',\n profile_name => :profile_name,\n action => 'chat'\n );\n IF v_result LIKE '%CONNECTED%' THEN\n DBMS_OUTPUT.PUT_LINE('Profile ' || :profile_name || ' is working.');\n ELSE\n DBMS_OUTPUT.PUT_LINE('Unexpected response: ' || v_result);\n END IF;\nEXCEPTION\n WHEN OTHERS THEN\n DBMS_OUTPUT.PUT_LINE('Profile test failed: ' || SQLERRM);\nEND;\n/\n```\n\n## Provider Outage Fallback — Using a Secondary Profile\n\nUse a failover function to try a primary profile and automatically fall back to a secondary profile when the primary provider is unreachable (ORA-20000 / ORA-29273).\n\n```sql\n-- Failover function: try primary profile, fall back to secondary\nCREATE OR REPLACE FUNCTION ai_generate_with_fallback(\n p_prompt IN VARCHAR2,\n p_primary_profile IN VARCHAR2 DEFAULT 'OPENAI_PROFILE',\n p_fallback_profile IN VARCHAR2 DEFAULT 'OCI_PROFILE'\n) RETURN CLOB AS\n v_result CLOB;\nBEGIN\n v_result := DBMS_CLOUD_AI.GENERATE(\n prompt => p_prompt,\n profile_name => p_primary_profile,\n action => 'chat'\n );\n RETURN v_result;\nEXCEPTION\n WHEN OTHERS THEN\n -- ORA-20000 / ORA-29273 = provider unreachable\n IF SQLCODE IN (-20000, -29273) THEN\n v_result := DBMS_CLOUD_AI.GENERATE(\n prompt => p_prompt,\n profile_name => p_fallback_profile,\n action => 'chat'\n );\n RETURN v_result;\n ELSE\n RAISE;\n END IF;\nEND;\n/\n```\n\n## OCI Region-Specific Endpoints\n\nOCI-backed profiles can specify `region` and, when needed, `provider_endpoint`.\n\n```sql\n-- OCI GenAI endpoints vary by region; always specify the correct region\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'OCI_FRANKFURT_PROFILE',\n attributes => '{\n \"provider\": \"oci\",\n \"credential_name\": \"OCI$RESOURCE_PRINCIPAL\",\n \"model\": \"cohere.command-r-plus\",\n \"oci_apiformat\": \"COHERE\",\n \"region\": \"eu-frankfurt-1\",\n \"provider_endpoint\": \"https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com\"\n }'\n );\nEND;\n/\n```\n\n## object_list Scope and Wildcard Syntax\n\nOmit the `name` field in an `object_list` entry to include all objects in a schema. Keep the list scoped to the tables relevant to the use case — large lists (more than 50 tables) slow down NL-to-SQL generation because the model must reason over more schema context.\n\n```sql\n-- Include all tables in a schema (wildcard)\n-- Use {\"owner\":\"HR\"} without a \"name\" field to include ALL objects in the schema\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'HR_AI',\n attributes => JSON_OBJECT(\n 'provider' VALUE 'openai',\n 'credential_name' VALUE 'OPENAI_CRED',\n 'model' VALUE 'gpt-4o',\n 'object_list' VALUE '[{\"owner\":\"HR\"}]' -- all tables in HR schema\n )\n );\nEND;\n/\n\n-- Include specific tables from multiple schemas\n-- object_list with explicit names:\n-- '[{\"owner\":\"HR\",\"name\":\"EMPLOYEES\"},{\"owner\":\"SALES\",\"name\":\"ORDERS\"}]'\n\n-- IMPORTANT: Large object_lists (> 50 tables) slow down NL-to-SQL generation\n-- because the model must reason over more schema context.\n-- Keep the list to the tables relevant to the use case.\n```\n\n## Rate Limiting Guidance\n\nAPI providers enforce rate limits (tokens per minute, requests per minute) — Oracle does not throttle on its side. For high-volume workloads, handle ORA-29273 with HTTP 429 by sleeping between retries. Prefer `UTL_TO_EMBEDDINGS` (batch) over per-row calls to minimize request count.\n\n```sql\n-- Check if the current call is hitting rate limits (ORA-29273 with HTTP 429)\n-- Implement a delay between batch calls:\nBEGIN\n FOR r IN (SELECT chunk_id, chunk_text FROM doc_chunks WHERE embedding IS NULL) LOOP\n BEGIN\n UPDATE doc_chunks\n SET embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(\n r.chunk_text,\n '{\"provider\":\"openai\",\"credential_name\":\"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\":\"text-embedding-3-small\"}'\n )\n WHERE chunk_id = r.chunk_id;\n EXCEPTION\n WHEN OTHERS THEN\n IF SQLCODE = -29273 THEN\n DBMS_SESSION.SLEEP(2); -- wait 2 seconds on rate limit\n -- Do not commit; retry will happen on next loop iteration\n ELSE RAISE;\n END IF;\n END;\n END LOOP;\n COMMIT;\nEND;\n/\n-- Better: use UTL_TO_EMBEDDINGS (batch API call) to minimize request count\n```\n\n## Profile Versioning and Rollback\n\nThere is no built-in versioning for AI profiles. Use a naming convention (e.g., `MYAPP_AI_V1`, `MYAPP_AI_V2`) and capture current settings before modifying a profile. To roll back, create the old profile under a new name and switch the session profile, or maintain a `_STABLE` profile that only changes after validation.\n\n```sql\n-- There is no built-in versioning for AI profiles.\n-- Best practice: use a naming convention to track versions\n-- e.g., MYAPP_AI_V1, MYAPP_AI_V2\n\n-- Before updating a profile, capture its current settings\nSELECT profile_name, status, description\nFROM user_cloud_ai_profiles\nWHERE profile_name = :profile_name;\n\nSELECT attribute_name, attribute_value\nFROM user_cloud_ai_profile_attributes\nWHERE profile_name = :profile_name\nORDER BY attribute_name;\n\n-- Update the profile (modifies in place)\nBEGIN\n DBMS_CLOUD_AI.SET_ATTRIBUTE(\n profile_name => 'MYAPP_AI',\n attribute_name => 'temperature',\n attribute_value => '0'\n );\nEND;\n/\n\n-- To rollback: create the old profile under a new name and switch the session profile\n-- Or: maintain a MYAPP_AI_STABLE profile that only changes after validation\n```\n\n## Security Best Practices\n\n- Store all API keys via `DBMS_CLOUD.CREATE_CREDENTIAL` — never hardcode keys in SQL or PL/SQL\n- Grant `EXECUTE ON DBMS_CLOUD` only to users who need to create credentials\n- Scope `object_list` to only the tables needed — the LLM sees table/column names and comments, not data\n- Rotate API keys via `DBMS_CLOUD.UPDATE_CREDENTIAL` without dropping/recreating the credential\n- On OCI, prefer `OCI$RESOURCE_PRINCIPAL` where available to avoid API key management overhead\n- For vector package calls, create and rotate credentials with `DBMS_VECTOR.CREATE_CREDENTIAL` or `DBMS_VECTOR_CHAIN.CREATE_CREDENTIAL` rather than assuming a `DBMS_CLOUD_AI` profile applies\n\n## Supported Embedding Models (as of Oracle 26ai)\n\n| Provider | Recommended Embedding Model | Dimensions |\n|---|---|---|\n| OpenAI | `text-embedding-3-small` | 1536 |\n| OpenAI | `text-embedding-3-large` | 3072 |\n| Cohere | `embed-english-v3.0` | 1024 |\n| OCI GenAI | `cohere.embed-english-v3.0` | 1024 |\n\n## Oracle Version Notes (19c vs 26ai)\n\n- **19c**: No DBMS_CLOUD_AI; external API calls required custom UTL_HTTP code\n- **26ai**: This guide targets the current AI profile APIs, including expanded provider support and attribute management through `SET_ATTRIBUTE` and `SET_ATTRIBUTES`\n\n## See Also\n\n- [SELECT AI in Oracle 26ai](../features/select-ai.md) — Using profiles for natural language to SQL\n- [DBMS_VECTOR and DBMS_VECTOR_CHAIN](../features/dbms-vector.md) — Using vector credentials and per-call parameters for embedding generation\n- [AI Vector Search in Oracle](../features/vector-search.md) — Storing and querying vector embeddings\n\n## Sources\n\n- [DBMS_CLOUD_AI Package Reference](https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-package.html)\n- [DBMS_CLOUD.CREATE_CREDENTIAL](https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-subprograms.html#GUID-748B56B8-6CDB-4C5D-9A84-F590738FB394)\n- [Oracle AI Vector Search — Configuring Providers](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/configure-provider.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14728,"content_sha256":"d1ae026714f45ff2561842ebbe754e32f88631b2016cdb293b220a38ffedfc69"},{"filename":"features/database-links.md","content":"# Oracle Database Links\n\n## Overview\n\nA **database link** (dblink) is a named connection descriptor stored in a local Oracle database that enables SQL statements to reference objects in a remote database as if they were local. A database link stores the connection information (host, port, service name) and — depending on the link type — the remote credentials used to establish the session.\n\nDatabase links are a built-in Oracle feature that predates most distributed database technology, and they remain a pragmatic solution for cross-database queries, distributed DML, and replication scenarios within an Oracle estate.\n\n**When database links are appropriate:**\n- Ad-hoc queries across two Oracle databases in the same trusted network\n- Scheduled batch jobs that consolidate data from multiple Oracle sources\n- Replication and synchronization between Oracle databases (often via materialized views)\n- Migration scenarios where data must be read from an old database during cut-over\n\n**When database links are NOT appropriate:**\n- High-frequency OLTP queries — the network round-trip overhead compounds quickly\n- Cross-database joins on large tables — data transfer volume is uncontrolled\n- Connections to non-Oracle databases directly (use heterogeneous services / generic connectivity instead)\n- Situations requiring strong security isolation — dblinks carry implicit trust\n\n---\n\n## Types of Database Links\n\n### Fixed User Link\n\nThe link always connects to the remote database as a specific, hardcoded user. Regardless of who executes a query through the link, they use the remote credentials stored in the link.\n\n```sql\nCREATE DATABASE LINK sales_db_link\nCONNECT TO remote_user IDENTIFIED BY \"remote_password\"\nUSING 'SALESDB'; -- TNS service name or connect string\n```\n\n### Connected User Link\n\nThe link connects to the remote database as the **same user** who is currently logged in to the local database. The remote database must have a matching account.\n\n```sql\nCREATE DATABASE LINK hr_db_link\nCONNECT TO CURRENT_USER\nUSING 'HRDB';\n```\n\nConnected user links are more secure than fixed user links because they do not embed credentials in the database and because each local user operates with their own remote privileges.\n\n### Shared Database Link\n\nA **shared link** reuses a single remote database session across multiple local sessions. This reduces connection overhead on the remote database at the cost of slightly more complex connection management.\n\n```sql\nCREATE SHARED DATABASE LINK shared_dw_link\nCONNECT TO dw_query_user IDENTIFIED BY \"dw_password\"\nUSING 'DWDB';\n```\n\n### Public vs Private Links\n\nBy default, a database link is **private** — accessible only to the user who created it. A **public** link is accessible to any database user.\n\n```sql\n-- Private link (owned by current user only)\nCREATE DATABASE LINK my_private_link\nCONNECT TO remote_user IDENTIFIED BY \"password\"\nUSING 'REMOTEDB';\n\n-- Public link (accessible by all database users)\nCREATE PUBLIC DATABASE LINK corp_shared_link\nCONNECT TO reporting_user IDENTIFIED BY \"rpt_password\"\nUSING 'REPORTDB';\n-- Requires CREATE PUBLIC DATABASE LINK privilege\n```\n\n---\n\n## TNS Connection Options\n\nThe `USING` clause accepts either a TNS alias (resolved via `tnsnames.ora` or LDAP) or an inline Easy Connect string:\n\n```sql\n-- Easy Connect string (no tnsnames.ora entry required)\nCREATE DATABASE LINK remote_via_ezconnect\nCONNECT TO app_user IDENTIFIED BY \"password\"\nUSING '//db-host.company.com:1521/ORCL';\n\n-- Full inline descriptor (TNS descriptor syntax)\nCREATE DATABASE LINK remote_full_descriptor\nCONNECT TO app_user IDENTIFIED BY \"password\"\nUSING '(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=db-host.company.com)(PORT=1521))\n (CONNECT_DATA=(SERVICE_NAME=ORCL)))';\n\n-- TNS alias from tnsnames.ora\nCREATE DATABASE LINK remote_via_tns\nCONNECT TO app_user IDENTIFIED BY \"password\"\nUSING 'PROD_DB';\n```\n\n---\n\n## Using Database Links in Queries\n\nOnce created, reference a remote object by appending `@\u003clink_name>` to the object name:\n\n### SELECT Across a Database Link\n\n```sql\n-- Select from a remote table\nSELECT employee_id, last_name, salary\nFROM employees@hr_db_link\nWHERE department_id = 90;\n\n-- Join local and remote tables\nSELECT l.order_id,\n l.order_date,\n r.customer_name,\n r.email\nFROM orders l\nJOIN customers@crm_db_link r ON r.customer_id = l.customer_id\nWHERE l.order_date > SYSDATE - 30;\n\n-- Use a synonym to hide the link name from application code\nCREATE SYNONYM remote_customers FOR customers@crm_db_link;\n\nSELECT * FROM remote_customers WHERE country_code = 'US';\n```\n\n### DML Across a Database Link\n\nOracle supports INSERT, UPDATE, DELETE, and MERGE on remote tables through database links:\n\n```sql\n-- Insert into a remote table\nINSERT INTO archive_orders@archive_db_link (order_id, order_date, amount)\nSELECT order_id, order_date, amount\nFROM orders_local\nWHERE order_date \u003c ADD_MONTHS(SYSDATE, -24);\n\n-- Update a remote record\nUPDATE customer_flags@crm_db_link\nSET is_active = 0\nWHERE last_order_date \u003c ADD_MONTHS(SYSDATE, -12);\n\n-- MERGE across a database link\nMERGE INTO product_catalog@dw_db_link target\nUSING (SELECT product_id, product_name, unit_price FROM products_local) src\nON (target.product_id = src.product_id)\nWHEN MATCHED THEN\n UPDATE SET target.unit_price = src.unit_price\nWHEN NOT MATCHED THEN\n INSERT (product_id, product_name, unit_price)\n VALUES (src.product_id, src.product_name, src.unit_price);\n\nCOMMIT;\n```\n\n### Calling Remote Procedures\n\n```sql\n-- Execute a stored procedure on the remote database\nBEGIN\n archive_pkg.purge_old_records@archive_db_link(p_cutoff_date => ADD_MONTHS(SYSDATE, -36));\nEND;\n/\n```\n\n---\n\n## Two-Phase Commit (Distributed Transactions)\n\nWhen a single Oracle transaction modifies data on **multiple databases** via database links, Oracle uses **two-phase commit (2PC)** to ensure atomicity across all sites.\n\n### How 2PC Works in Oracle\n\n1. **Prepare phase:** The local database (coordinator) asks each remote database (participant) whether it is ready to commit.\n2. **Commit phase:** If all participants report ready, the coordinator instructs everyone to commit. If any participant reports not ready (or times out), all sites roll back.\n\nThe coordinator records the 2PC decision in `DBA_2PC_PENDING` before the final commit or rollback so that the transaction can be resolved manually if a participant becomes unreachable.\n\n```sql\n-- Distributed transaction touching two databases\nBEGIN\n -- Local insert\n INSERT INTO local_orders (order_id, amount) VALUES (9001, 1500.00);\n\n -- Remote insert (triggers 2PC coordination)\n INSERT INTO order_archive@archive_db_link (order_id, amount) VALUES (9001, 1500.00);\n\n COMMIT; -- Oracle negotiates 2PC automatically\nEND;\n/\n```\n\n### Monitoring and Resolving In-Doubt Transactions\n\n```sql\n-- View in-doubt distributed transactions\nSELECT local_tran_id, global_tran_id, state, mixed, host, db_user, advice\nFROM dba_2pc_pending;\n\n-- Manually force commit of an in-doubt transaction (use only when instructed)\n-- This is only safe when you have confirmed the remote side committed\nCOMMIT FORCE '10.13.3.10.1'; -- use the local_tran_id from DBA_2PC_PENDING\n\n-- Manually force rollback\nROLLBACK FORCE '10.13.3.10.1';\n\n-- Clean up after resolution\nDELETE FROM dba_2pc_pending WHERE local_tran_id = '10.13.3.10.1';\nEXEC DBMS_TRANSACTION.PURGE_LOST_DB_ENTRY('10.13.3.10.1');\n```\n\n**Important:** Never manually force-commit or force-rollback a distributed transaction without first confirming the state of the remote participant. Forcing the wrong outcome creates data inconsistencies that are hard to detect and correct.\n\n---\n\n## Performance Implications\n\n### The Remote-First Execution Problem\n\nOracle's optimizer evaluates distributed queries based on local statistics. It often underestimates remote table sizes because remote statistics are not always current. This can result in the optimizer fetching large result sets across the network instead of applying filters remotely.\n\n```sql\n-- PROBLEMATIC: Oracle may push the join to the remote side, fetching all of\n-- 'orders' (potentially millions of rows) across the network\nSELECT o.order_id, c.customer_name\nFROM orders o\nJOIN customers@remote_db c ON c.customer_id = o.customer_id\nWHERE o.order_date > SYSDATE - 7;\n\n-- BETTER: Force the filtering to happen locally first, then join to remote\nSELECT o.order_id, c.customer_name\nFROM (SELECT order_id, customer_id FROM orders WHERE order_date > SYSDATE - 7) o\nJOIN customers@remote_db c ON c.customer_id = o.customer_id;\n```\n\n### The `DRIVING_SITE` Hint\n\nThe `DRIVING_SITE` hint instructs Oracle to execute the join at the specified database location, reducing data movement:\n\n```sql\n-- Execute the query at the remote site (remote data is large; local filter is selective)\nSELECT /*+ DRIVING_SITE(c) */\n o.order_id, c.customer_name\nFROM orders o\nJOIN customers@remote_db c ON c.customer_id = o.customer_id\nWHERE c.country_code = 'US';\n```\n\n### Using DB Links with Materialized Views for Performance\n\nInstead of live queries through a database link, consider pulling data via an MV:\n\n```sql\n-- Create a local MV that refreshes from the remote database daily\nCREATE MATERIALIZED VIEW mv_remote_customers\nBUILD IMMEDIATE\nREFRESH COMPLETE ON DEMAND\nAS\nSELECT customer_id, customer_name, country_code, email\nFROM customers@crm_db_link;\n\n-- Schedule refresh via DBMS_SCHEDULER\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'REFRESH_REMOTE_CUSTOMERS_MV',\n job_type => 'PLSQL_BLOCK',\n job_action => 'BEGIN DBMS_MVIEW.REFRESH(''MV_REMOTE_CUSTOMERS'', ''C''); END;',\n repeat_interval => 'FREQ=DAILY;BYHOUR=3;BYMINUTE=0;BYSECOND=0',\n enabled => TRUE\n );\nEND;\n/\n```\n\n---\n\n## Managing Database Links\n\n```sql\n-- View database links owned by current user\nSELECT db_link, username, host, created\nFROM user_db_links\nORDER BY db_link;\n\n-- View all database links (DBA view)\nSELECT owner, db_link, username, host, created\nFROM dba_db_links\nORDER BY owner, db_link;\n\n-- Test a database link\nSELECT * FROM dual@hr_db_link;\n-- Expected result: one row, column X = 'X'\n\n-- Check current session's open database link connections\nSELECT db_link\nFROM v$dblink;\n\n-- Close an open database link without disconnecting the session\nALTER SESSION CLOSE DATABASE LINK hr_db_link;\n\n-- Drop a database link\nDROP DATABASE LINK hr_db_link;\nDROP PUBLIC DATABASE LINK corp_shared_link;\n```\n\n---\n\n## Security Risks and Best Practices\n\n### Risks\n\n1. **Credential exposure:** Fixed user links store the remote password in an encrypted but accessible form in `SYS.LINK db — Skillopedia . A DBA with `SELECT ANY DICTIONARY` access can potentially extract link credentials. This is a well-known concern — treat fixed user link credentials as shared secrets with limited lifetime.\n\n2. **Privilege escalation:** A user with access to a database link pointing to a remote DBA account can execute arbitrary DDL on the remote database.\n\n3. **Audit blind spots:** DML executed through a database link is recorded in the remote database's audit trail under the remote link user, not the local initiating user. This breaks end-to-end accountability unless both sides are audited and correlated.\n\n4. **Lateral movement:** A compromised application schema with access to a fixed user dblink becomes a pivot point to a second database.\n\n### Best Practices\n\n- **Prefer connected user links** over fixed user links for user-facing applications. Each local user's identity carries through to the remote database, preserving audit trails and enforcing remote-side row-level security.\n- **Create dedicated remote users for database links** with only the minimum privileges required (SELECT on specific tables, not CONNECT RESOURCE or DBA).\n- **Rotate fixed user link passwords on a schedule.** Use Oracle Vault or a secrets manager. To change the password, recreate the link:\n\n```sql\n-- Recreate a database link to update the password\nDROP DATABASE LINK old_link;\nCREATE DATABASE LINK old_link\nCONNECT TO remote_user IDENTIFIED BY \"new_password\"\nUSING 'REMOTEDB';\n```\n\n- **Audit database link usage.** Enable fine-grained auditing or Oracle Audit Vault to capture all cross-database operations.\n\n```sql\n-- Enable audit for database link operations\nAUDIT SELECT TABLE, INSERT TABLE, UPDATE TABLE, DELETE TABLE\nBY ACCESS\nWHENEVER SUCCESSFUL;\n```\n\n- **Never create public database links pointing to privileged remote accounts.** Any database user (including those created by application frameworks, tooling, or attackers) can use a public link.\n- **Review `DBA_DB_LINKS` regularly.** Remove links that are no longer used. An unused link pointing to a decommissioned system is a latent security and connectivity risk.\n- **Firewall remote database ports** so that only the Oracle listener port is accessible, and only from the specific source database hosts. Database link connections travel through standard Oracle Net, so standard network controls apply.\n- **Use Oracle Network Encryption (ASO/TLS)** for database link traffic on untrusted networks. Database link traffic is cleartext by default.\n\n```sql\n-- sqlnet.ora on the client (initiating) side\n-- Add: SQLNET.ENCRYPTION_CLIENT = REQUIRED\n-- SQLNET.ENCRYPTION_TYPES_CLIENT = (AES256)\n```\n\n---\n\n## Common Mistakes and How to Avoid Them\n\n**Mistake 1: Using fixed user links pointing to powerful accounts**\nFixed user links pointing to a `DBA` or `CONNECT RESOURCE` account grant anyone who can access the link full control of the remote database. Always create a minimal-privilege remote account for dblink use.\n\n**Mistake 2: Live cross-database joins in OLTP code paths**\nEvery row retrieved through a database link incurs a network round-trip. A join that scans 100,000 remote rows transfers the entire result set across the network. Audit all production code paths that use `@link_name` and replace hot paths with scheduled MV refreshes or local copies.\n\n**Mistake 3: Ignoring `DBA_2PC_PENDING` entries**\nIn-doubt transactions left unresolved accumulate in `DBA_2PC_PENDING` and consume resources (locks, rollback segment entries). Build a monitoring alert for non-empty `DBA_2PC_PENDING`. RECO (the Recoverer) process should resolve them automatically when connectivity is restored, but in some cases manual intervention is needed.\n\n**Mistake 4: Creating public database links in multi-tenant environments**\nIn an Oracle Multitenant (CDB/PDB) environment, a public database link in a PDB is accessible to all users of that PDB. Treat public links with the same caution as granting DBA to public. Prefer private links or application-level connection management.\n\n**Mistake 5: Not testing links after network or firewall changes**\nDatabase link failures surface as `ORA-12170: TNS:Connect timeout` or `ORA-12541: TNS:no listener` errors at query time, not at creation time. After any network change, test all active links with `SELECT * FROM dual@\u003clink_name>`.\n\n**Mistake 6: Storing the link password in application scripts**\nSome teams create database links via scripts with hardcoded passwords. These scripts often end up in version control. Use environment variables or Oracle Vault to inject credentials at deployment time, and avoid committing connection scripts with embedded passwords.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database Administrator's Guide: Managing Distributed Databases — Database Links 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-a-distributed-database.html)\n- [Oracle Database SQL Language Reference: CREATE DATABASE LINK 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-DATABASE-LINK.html)\n- [Oracle Database Heterogeneous Connectivity User's Guide 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/heter/index.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16362,"content_sha256":"c841008987eac9223353d5d363dbe0c0af0b94e9bc1107756de461656bfb16e7"},{"filename":"features/dbms-scheduler.md","content":"# Oracle DBMS_SCHEDULER\n\n## Overview\n\n`DBMS_SCHEDULER` is Oracle's enterprise job scheduling framework, introduced in Oracle 10g to replace the older `DBMS_JOB` package. It provides a rich feature set for scheduling PL/SQL code, stored procedures, executables, and scripts — either on a time-based calendar expression, in response to external events, or as part of dependency chains.\n\nKey advantages over `DBMS_JOB`:\n- Named, visible objects (queryable from `DBA_SCHEDULER_*` views)\n- Calendar-based recurrence expressions (cron-like but more powerful)\n- Job classes for resource management and logging control\n- Windows and window groups for maintenance-period scheduling\n- Job chains for workflow orchestration with dependencies\n- Event-based triggering (queue events, file arrival, custom events)\n- External jobs running OS executables\n- Built-in email/alert notification framework\n\n---\n\n## Core Object Hierarchy\n\n```\nSCHEDULES — reusable recurrence definitions\nJOB CLASSES — group jobs for resource/logging policy\nWINDOWS — time windows that activate resource plans\nWINDOW GROUPS — collections of windows\nPROGRAMS — reusable action definitions (what to run)\nJOBS — the scheduled unit; references a program + schedule\nCHAINS — dependency graphs of job steps\n```\n\n---\n\n## Schedules\n\nA **schedule** defines *when* something runs. It can be referenced by multiple jobs, avoiding repetition.\n\n```sql\n-- Simple daily schedule at 2:00 AM\nBEGIN\n DBMS_SCHEDULER.CREATE_SCHEDULE(\n schedule_name => 'DAILY_2AM',\n start_date => SYSTIMESTAMP,\n repeat_interval => 'FREQ=DAILY;BYHOUR=2;BYMINUTE=0;BYSECOND=0',\n end_date => NULL, -- NULL = run forever\n comments => 'Runs every day at 02:00 server time'\n );\nEND;\n/\n\n-- Weekday business hours — every 15 minutes, Mon-Fri, 8 AM to 6 PM\nBEGIN\n DBMS_SCHEDULER.CREATE_SCHEDULE(\n schedule_name => 'WEEKDAY_BUSINESS_HOURS_15MIN',\n repeat_interval => 'FREQ=MINUTELY;INTERVAL=15;BYDAY=MON,TUE,WED,THU,FRI;BYHOUR=8,9,10,11,12,13,14,15,16,17',\n comments => 'Every 15 minutes on weekdays during business hours'\n );\nEND;\n/\n\n-- Last day of every month\nBEGIN\n DBMS_SCHEDULER.CREATE_SCHEDULE(\n schedule_name => 'MONTHLY_LAST_DAY',\n repeat_interval => 'FREQ=MONTHLY;BYMONTHDAY=-1;BYHOUR=23;BYMINUTE=0;BYSECOND=0',\n comments => 'Last day of month at 23:00'\n );\nEND;\n/\n\n-- Every quarter (1st of Jan, Apr, Jul, Oct)\nBEGIN\n DBMS_SCHEDULER.CREATE_SCHEDULE(\n schedule_name => 'QUARTERLY',\n repeat_interval => 'FREQ=YEARLY;BYMONTH=1,4,7,10;BYMONTHDAY=1;BYHOUR=6;BYMINUTE=0;BYSECOND=0',\n comments => 'Quarterly on the 1st at 06:00'\n );\nEND;\n/\n```\n\n### Calendar Expression Quick Reference\n\n| Expression | Meaning |\n|---|---|\n| `FREQ=SECONDLY;INTERVAL=30` | Every 30 seconds |\n| `FREQ=MINUTELY;INTERVAL=5` | Every 5 minutes |\n| `FREQ=HOURLY` | Every hour |\n| `FREQ=DAILY;BYHOUR=0` | Daily at midnight |\n| `FREQ=WEEKLY;BYDAY=MON` | Every Monday |\n| `FREQ=MONTHLY;BYMONTHDAY=1` | 1st of every month |\n| `FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=31` | December 31 every year |\n| `FREQ=DAILY;BYDAY=MON,TUE,WED,THU,FRI` | Every weekday |\n\n---\n\n## Programs\n\nA **program** encapsulates the action to be executed. Defining programs separately from jobs allows reuse.\n\n```sql\n-- PL/SQL stored procedure program\nBEGIN\n DBMS_SCHEDULER.CREATE_PROGRAM(\n program_name => 'REFRESH_SUMMARY_TABLES_PROG',\n program_type => 'STORED_PROCEDURE',\n program_action => 'etl_pkg.refresh_summary_tables',\n number_of_arguments => 0,\n enabled => TRUE,\n comments => 'Calls the ETL refresh procedure'\n );\nEND;\n/\n\n-- PL/SQL block program with an argument\nBEGIN\n DBMS_SCHEDULER.CREATE_PROGRAM(\n program_name => 'ARCHIVE_OLD_RECORDS_PROG',\n program_type => 'PLSQL_BLOCK',\n program_action => 'BEGIN archive_pkg.run(:threshold_days); END;',\n number_of_arguments => 1,\n enabled => FALSE -- enable after defining arguments\n );\n\n DBMS_SCHEDULER.DEFINE_PROGRAM_ARGUMENT(\n program_name => 'ARCHIVE_OLD_RECORDS_PROG',\n argument_position => 1,\n argument_name => 'threshold_days',\n argument_type => 'NUMBER',\n default_value => '90'\n );\n\n DBMS_SCHEDULER.ENABLE(name => 'ARCHIVE_OLD_RECORDS_PROG');\nEND;\n/\n\n-- External executable program (runs an OS script)\nBEGIN\n DBMS_SCHEDULER.CREATE_PROGRAM(\n program_name => 'BACKUP_SCRIPT_PROG',\n program_type => 'EXECUTABLE',\n program_action => '/opt/oracle/scripts/run_backup.sh',\n enabled => TRUE\n );\nEND;\n/\n```\n\n---\n\n## Jobs\n\nA **job** ties together a program (or inline action) with a schedule (or inline schedule).\n\n### Simple Inline Job\n\n```sql\n-- Quickest form: inline action and inline schedule\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'NIGHTLY_STATS_GATHER',\n job_type => 'PLSQL_BLOCK',\n job_action => 'BEGIN DBMS_STATS.GATHER_SCHEMA_STATS(''APPSCHEMA''); END;',\n start_date => SYSTIMESTAMP,\n repeat_interval => 'FREQ=DAILY;BYHOUR=1;BYMINUTE=0;BYSECOND=0',\n end_date => NULL,\n enabled => TRUE,\n auto_drop => FALSE, -- keep job definition after it runs\n comments => 'Gather schema statistics nightly at 1 AM'\n );\nEND;\n/\n```\n\n### Job Referencing a Named Program and Schedule\n\n```sql\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'REFRESH_SUMMARY_JOB',\n program_name => 'REFRESH_SUMMARY_TABLES_PROG',\n schedule_name => 'DAILY_2AM',\n job_class => 'BATCH_JOB_CLASS', -- see Job Classes section\n enabled => TRUE,\n auto_drop => FALSE,\n comments => 'Nightly ETL refresh'\n );\nEND;\n/\n```\n\n### One-Time Job\n\n```sql\n-- Run once at a specific future time\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'ONE_TIME_MIGRATION',\n job_type => 'STORED_PROCEDURE',\n job_action => 'migration_pkg.run_v2_migration',\n start_date => TO_TIMESTAMP_TZ('2026-03-15 03:00:00 -05:00', 'YYYY-MM-DD HH24:MI:SS TZH:TZM'),\n enabled => TRUE,\n auto_drop => TRUE -- remove job definition after it completes\n );\nEND;\n/\n```\n\n### Managing Jobs\n\n```sql\n-- Enable / disable\nEXEC DBMS_SCHEDULER.ENABLE('NIGHTLY_STATS_GATHER');\nEXEC DBMS_SCHEDULER.DISABLE('NIGHTLY_STATS_GATHER');\n\n-- Run immediately (ad-hoc execution, does not reset next_run_date)\nEXEC DBMS_SCHEDULER.RUN_JOB('NIGHTLY_STATS_GATHER');\n\n-- Stop a running job\nEXEC DBMS_SCHEDULER.STOP_JOB('NIGHTLY_STATS_GATHER', force => FALSE);\n\n-- Drop a job\nEXEC DBMS_SCHEDULER.DROP_JOB('ONE_TIME_MIGRATION');\n\n-- Set a job attribute after creation\nBEGIN\n DBMS_SCHEDULER.SET_ATTRIBUTE(\n name => 'NIGHTLY_STATS_GATHER',\n attribute => 'max_failures',\n value => 3 -- disable job after 3 consecutive failures\n );\n DBMS_SCHEDULER.SET_ATTRIBUTE(\n name => 'NIGHTLY_STATS_GATHER',\n attribute => 'max_run_duration',\n value => INTERVAL '2' HOUR -- alert/kill if runs > 2 hours\n );\nEND;\n/\n```\n\n---\n\n## Job Classes\n\nJob classes control **resource consumer group membership** and **logging behavior** for a group of jobs.\n\n```sql\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB_CLASS(\n job_class_name => 'BATCH_JOB_CLASS',\n resource_consumer_group => 'BATCH_GROUP', -- maps to Resource Manager group\n service => NULL, -- run on any instance in RAC\n logging_level => DBMS_SCHEDULER.LOGGING_FULL,\n log_history => 30, -- keep log entries for 30 days\n comments => 'Long-running batch jobs with full logging'\n );\n\n DBMS_SCHEDULER.CREATE_JOB_CLASS(\n job_class_name => 'LIGHT_JOB_CLASS',\n logging_level => DBMS_SCHEDULER.LOGGING_RUNS, -- log run records only\n log_history => 7,\n comments => 'Short, frequent jobs — minimal logging'\n );\nEND;\n/\n```\n\nLogging levels:\n- `LOGGING_OFF` — no logging\n- `LOGGING_FAILED_RUNS` — only log failed runs\n- `LOGGING_RUNS` — log every run (start + end)\n- `LOGGING_FULL` — log runs plus all operations (create, alter, drop)\n\n---\n\n## Windows\n\nA **window** is a time period during which a specific Resource Manager plan is active. Jobs can be assigned to a window, running only when the window is open.\n\n```sql\n-- Maintenance window: weeknights 10 PM to 6 AM\nBEGIN\n DBMS_SCHEDULER.CREATE_WINDOW(\n window_name => 'WEEKNIGHT_MAINTENANCE',\n resource_plan => 'MAINTENANCE_PLAN',\n repeat_interval => 'FREQ=WEEKLY;BYDAY=MON,TUE,WED,THU,FRI;BYHOUR=22;BYMINUTE=0;BYSECOND=0',\n duration => INTERVAL '8' HOUR,\n window_priority => LOW,\n comments => 'Weeknight maintenance window 22:00-06:00'\n );\nEND;\n/\n\n-- Weekend window\nBEGIN\n DBMS_SCHEDULER.CREATE_WINDOW(\n window_name => 'WEEKEND_MAINTENANCE',\n resource_plan => 'MAINTENANCE_PLAN',\n repeat_interval => 'FREQ=WEEKLY;BYDAY=SAT;BYHOUR=6;BYMINUTE=0;BYSECOND=0',\n duration => INTERVAL '48' HOUR,\n window_priority => HIGH\n );\nEND;\n/\n\n-- Group windows for convenience\nBEGIN\n DBMS_SCHEDULER.CREATE_WINDOW_GROUP(\n group_name => 'ALL_MAINTENANCE_WINDOWS',\n window_list => 'WEEKNIGHT_MAINTENANCE,WEEKEND_MAINTENANCE'\n );\nEND;\n/\n```\n\n---\n\n## Job Chains (Dependency Workflows)\n\nChains allow jobs to run in sequence or parallel with conditional branching based on prior step results. This is Oracle Scheduler's workflow engine.\n\n```sql\n-- Step 1: Create the chain\nBEGIN\n DBMS_SCHEDULER.CREATE_CHAIN(\n chain_name => 'ETL_PIPELINE_CHAIN',\n comments => 'Extract -> Transform -> Load with error notification'\n );\nEND;\n/\n\n-- Step 2: Define chain steps (each step references a program)\nBEGIN\n DBMS_SCHEDULER.DEFINE_CHAIN_STEP(\n chain_name => 'ETL_PIPELINE_CHAIN',\n step_name => 'EXTRACT',\n program_name => 'EXTRACT_DATA_PROG'\n );\n\n DBMS_SCHEDULER.DEFINE_CHAIN_STEP(\n chain_name => 'ETL_PIPELINE_CHAIN',\n step_name => 'TRANSFORM',\n program_name => 'TRANSFORM_DATA_PROG'\n );\n\n DBMS_SCHEDULER.DEFINE_CHAIN_STEP(\n chain_name => 'ETL_PIPELINE_CHAIN',\n step_name => 'LOAD',\n program_name => 'LOAD_DATA_PROG'\n );\n\n DBMS_SCHEDULER.DEFINE_CHAIN_STEP(\n chain_name => 'ETL_PIPELINE_CHAIN',\n step_name => 'NOTIFY_FAILURE',\n program_name => 'SEND_FAILURE_EMAIL_PROG'\n );\nEND;\n/\n\n-- Step 3: Define rules (transitions between steps)\nBEGIN\n -- Start EXTRACT when chain starts\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'START_EXTRACT',\n condition => 'TRUE',\n action => 'START EXTRACT',\n comments => 'Always start with extraction'\n );\n\n -- TRANSFORM runs only if EXTRACT succeeded\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'EXTRACT_SUCCESS',\n condition => 'EXTRACT COMPLETED SUCCESSFULLY',\n action => 'START TRANSFORM'\n );\n\n -- LOAD runs only if TRANSFORM succeeded\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'TRANSFORM_SUCCESS',\n condition => 'TRANSFORM COMPLETED SUCCESSFULLY',\n action => 'START LOAD'\n );\n\n -- If EXTRACT or TRANSFORM fails, send notification\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'ANY_FAILURE',\n condition => 'EXTRACT FAILED OR TRANSFORM FAILED OR LOAD FAILED',\n action => 'START NOTIFY_FAILURE'\n );\n\n -- End chain after LOAD success or after notification\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'END_SUCCESS',\n condition => 'LOAD COMPLETED SUCCESSFULLY',\n action => 'END'\n );\n\n DBMS_SCHEDULER.DEFINE_CHAIN_RULE(\n chain_name => 'ETL_PIPELINE_CHAIN',\n rule_name => 'END_FAILURE',\n condition => 'NOTIFY_FAILURE COMPLETED',\n action => 'END'\n );\nEND;\n/\n\n-- Step 4: Enable the chain\nEXEC DBMS_SCHEDULER.ENABLE('ETL_PIPELINE_CHAIN');\n\n-- Step 5: Schedule a job to run the chain\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'NIGHTLY_ETL_JOB',\n job_type => 'CHAIN',\n job_action => 'ETL_PIPELINE_CHAIN',\n schedule_name => 'DAILY_2AM',\n enabled => TRUE\n );\nEND;\n/\n```\n\n---\n\n## Event-Based Scheduling\n\nJobs can be triggered by Oracle AQ events instead of (or in addition to) time-based schedules.\n\n```sql\n-- Create an event queue for job triggers\nBEGIN\n DBMS_AQADM.CREATE_QUEUE_TABLE(\n queue_table => 'scheduler_event_tab',\n queue_payload_type => 'SYS.SCHEDULER$_EVENT_INFO',\n multiple_consumers => TRUE\n );\n DBMS_AQADM.CREATE_QUEUE(\n queue_name => 'file_arrival_events_q',\n queue_table => 'scheduler_event_tab'\n );\n DBMS_AQADM.START_QUEUE(queue_name => 'file_arrival_events_q');\nEND;\n/\n\n-- Create an event-triggered job\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'PROCESS_FILE_ON_ARRIVAL',\n program_name => 'PROCESS_FILE_PROG',\n event_condition => 'tab.user_data.event_name = ''FILE_ARRIVED''',\n queue_spec => 'file_arrival_events_q',\n enabled => TRUE,\n auto_drop => FALSE\n );\nEND;\n/\n```\n\n---\n\n## Logging and Monitoring\n\n### Key Views\n\n```sql\n-- Current job status and next scheduled run\nSELECT job_name,\n state,\n enabled,\n run_count,\n failure_count,\n last_start_date,\n last_run_duration,\n next_run_date\nFROM dba_scheduler_jobs\nWHERE owner = 'APPSCHEMA'\nORDER BY next_run_date;\n\n-- Detailed run history with errors\nSELECT job_name,\n log_date,\n status,\n error# AS error_code,\n actual_start_date,\n run_duration,\n cpu_used\nFROM dba_scheduler_job_run_details\nWHERE owner = 'APPSCHEMA'\n AND log_date > SYSDATE - 7\nORDER BY log_date DESC;\n\n-- All scheduler log entries (create, alter, enable, run)\nSELECT log_date, owner, job_name, operation, status, additional_info\nFROM dba_scheduler_job_log\nWHERE log_date > SYSDATE - 1\nORDER BY log_date DESC;\n\n-- Currently running jobs\nSELECT job_name, session_id, running_instance, elapsed_time, cpu_used\nFROM dba_scheduler_running_jobs;\n\n-- Chain step history\nSELECT job_name, chain_name, step_name, status, start_date, end_date, error_code\nFROM dba_scheduler_job_run_details\nWHERE job_name = 'NIGHTLY_ETL_JOB'\nORDER BY start_date DESC;\n\n-- Window history\nSELECT window_name, actual_start_date, actual_duration, completed\nFROM dba_scheduler_window_log\nORDER BY actual_start_date DESC;\n```\n\n### Purging Old Log Entries\n\n```sql\n-- Purge logs older than 30 days for a specific job\nBEGIN\n DBMS_SCHEDULER.PURGE_LOG(\n log_history => 30,\n which_log => 'JOB_AND_WINDOW_LOG',\n job_name => 'APPSCHEMA.NIGHTLY_STATS_GATHER'\n );\nEND;\n/\n\n-- Global purge for all jobs\nBEGIN\n DBMS_SCHEDULER.PURGE_LOG(log_history => 30);\nEND;\n/\n```\n\n---\n\n## Error Handling\n\n```sql\n-- Set max_failures to auto-disable after repeated failures\nBEGIN\n DBMS_SCHEDULER.SET_ATTRIBUTE('NIGHTLY_ETL_JOB', 'max_failures', 3);\nEND;\n/\n\n-- Email notification on job failure using DBMS_SCHEDULER notifications\nBEGIN\n DBMS_SCHEDULER.ADD_JOB_EMAIL_NOTIFICATION(\n job_name => 'NIGHTLY_ETL_JOB',\n recipients => '[email protected]',\n sender => '[email protected]',\n subject => 'Scheduler job failed: %job_name%',\n body => 'Job %job_name% failed at %event_timestamp%. Error: %error_message%',\n events => 'JOB_FAILED'\n );\nEND;\n/\n\n-- PL/SQL error handling within a job action\nCREATE OR REPLACE PROCEDURE safe_etl_job AS\nBEGIN\n etl_pkg.run_full_load;\nEXCEPTION\n WHEN OTHERS THEN\n -- Log to an application error table\n INSERT INTO etl_job_errors (job_name, error_time, error_msg, error_stack)\n VALUES ('NIGHTLY_ETL', SYSTIMESTAMP, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_STACK);\n COMMIT;\n -- Re-raise so DBMS_SCHEDULER records the failure\n RAISE;\nEND safe_etl_job;\n/\n```\n\n---\n\n## External Jobs\n\nExternal jobs run OS executables under a credential. The Oracle Scheduler Agent (`extjob` / `schagent`) must be running on the target host.\n\n```sql\n-- Create a database credential for the OS user\nBEGIN\n DBMS_SCHEDULER.CREATE_CREDENTIAL(\n credential_name => 'ORACLE_OS_CRED',\n username => 'oracle',\n password => 'os_password_here'\n );\nEND;\n/\n\n-- Create an external job\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'WEEKLY_EXPORT_JOB',\n job_type => 'EXECUTABLE',\n job_action => '/opt/oracle/scripts/weekly_export.sh',\n credential_name => 'ORACLE_OS_CRED',\n destination => 'LOCAL', -- or a remote host name registered with DBMS_SCHEDULER\n schedule_name => 'WEEKLY_SUNDAY',\n enabled => TRUE\n );\nEND;\n/\n```\n\n---\n\n## Best Practices\n\n- **Name jobs clearly and consistently.** Use a naming convention like `\u003cSCHEMA>_\u003cPURPOSE>_\u003cFREQUENCY>_JOB` (e.g., `HR_PURGE_OLD_LOGS_DAILY_JOB`). Scheduler objects are visible in DBA views — good names pay dividends during incident response.\n- **Always set `auto_drop => FALSE`** for recurring jobs. The default `TRUE` drops the job after the first run, which is rarely what you want for scheduled work.\n- **Use named schedules and programs** instead of inlining everything in the job. This promotes reuse and makes bulk rescheduling trivial: change the schedule object, and all jobs using it update automatically.\n- **Set `max_run_duration`** for jobs with SLA requirements. If a job runs longer than expected, it likely indicates a problem and should alert the team.\n- **Use job classes** to assign jobs to appropriate Resource Manager consumer groups. Batch jobs should not compete with OLTP on CPU resources.\n- **Test calendar expressions** with `DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING` before creating jobs.\n\n```sql\n-- Validate a calendar expression and see the next 5 scheduled times\nDECLARE\n next_run TIMESTAMP WITH TIME ZONE := SYSTIMESTAMP;\nBEGIN\n FOR i IN 1..5 LOOP\n DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING(\n calendar_string => 'FREQ=WEEKLY;BYDAY=MON,WED,FRI;BYHOUR=8;BYMINUTE=30',\n start_date => SYSTIMESTAMP,\n return_date_after => next_run,\n next_run_date => next_run\n );\n DBMS_OUTPUT.PUT_LINE('Run ' || i || ': ' || TO_CHAR(next_run, 'YYYY-MM-DD HH24:MI:SS TZH:TZM'));\n END LOOP;\nEND;\n/\n```\n\n---\n\n## Common Mistakes and How to Avoid Them\n\n**Mistake 1: Using `DBMS_JOB` in new code**\n`DBMS_JOB` still works but is a legacy interface with no support for most advanced features. Always use `DBMS_SCHEDULER` in new development. Migrate existing `DBMS_JOB` entries by recreating them as `DBMS_SCHEDULER` jobs rather than relying on undocumented internal packages.\n\n**Mistake 2: Not specifying time zones in `start_date`**\nCalendar expressions are evaluated relative to the time zone in `start_date`. If you create a job with a bare `SYSDATE` (no time zone), behavior during daylight saving transitions is undefined. Always use `SYSTIMESTAMP` or a `TIMESTAMP WITH TIME ZONE` literal with an explicit offset.\n\n**Mistake 3: Setting `enabled => TRUE` before fully configuring the job**\nIf you enable a job whose `start_date` is in the past and whose `repeat_interval` calculates a start in the past, it may fire immediately upon enabling. Create jobs in a disabled state, complete configuration, then explicitly call `DBMS_SCHEDULER.ENABLE`.\n\n**Mistake 4: Forgetting to enable the chain before scheduling it**\nA job of type `CHAIN` will fail at runtime if the referenced chain is not enabled. Enable the chain object itself (`DBMS_SCHEDULER.ENABLE('MY_CHAIN')`) separately from enabling the job.\n\n**Mistake 5: Assuming the log is always complete**\nThe `logging_level` on the job class controls what is recorded. A class with `LOGGING_FAILED_RUNS` will not show successful runs in `DBA_SCHEDULER_JOB_RUN_DETAILS`. If a job appears to never run, check both its `state` column and the logging level of its class.\n\n**Mistake 6: Running privileged operations in jobs owned by application schemas**\nJobs run with the privileges of their owner schema. A job owned by `APPSCHEMA` cannot call `DBMS_STATS.GATHER_DATABASE_STATS` unless `APPSCHEMA` has the `ANALYZE ANY` privilege. Create DBA-owned jobs for operations requiring elevated privileges, or grant only the minimum necessary privilege to the application schema.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [DBMS_SCHEDULER — Oracle Database PL/SQL Packages and Types Reference 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_SCHEDULER.html)\n- [Oracle Database Administrator's Guide: Scheduling Jobs with Oracle Scheduler 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/scheduling-jobs-with-oracle-scheduler.html)\n- [Oracle Scheduler Concepts — Oracle Database 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/oracle-scheduler-concepts.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22165,"content_sha256":"a6728ba19fb6a1270588f1331b5a3a43d145111bd1225bab46e43c9f265ff406"},{"filename":"features/dbms-vector.md","content":"# DBMS_VECTOR, DBMS_VECTOR_CHAIN, and DBMS_HYBRID_VECTOR in Oracle 26ai\n\nOracle 26ai provides three PL/SQL package families for AI vector operations:\n\n- **`DBMS_VECTOR`** — utility functions: generate embeddings, convert vectors, manage vector indexes\n- **`DBMS_VECTOR_CHAIN`** — pipeline functions for end-to-end RAG workflows: chunk text, generate embeddings, store, search, summarize, and generate responses\n- **`DBMS_HYBRID_VECTOR`** — hybrid search functions and helpers for search paths that combine vector and text retrieval\n\nThe vector packages use vector credentials and per-call JSON parameters rather than `DBMS_CLOUD_AI` profiles.\n\n## DBMS_VECTOR: Core Functions\n\n### UTL_TO_EMBEDDING — Generate a Single Embedding\n\n```sql\n-- Generate an embedding for a single text string\nDECLARE\n v_embed VECTOR;\nBEGIN\n v_embed := DBMS_VECTOR.UTL_TO_EMBEDDING(\n data => 'Oracle Database is a relational database management system.',\n params => JSON_OBJECT(\n 'provider' VALUE 'openai',\n 'credential_name' VALUE 'YOUR_SCHEMA.OPENAI_VEC_CRED',\n 'model' VALUE 'text-embedding-3-small'\n )\n );\n -- v_embed is now a VECTOR(1536, FLOAT32)\n DBMS_OUTPUT.PUT_LINE('Dimensions: ' || VECTOR_DIMENSION(v_embed));\nEND;\n/\n```\n\n### UTL_TO_EMBEDDINGS — Batch Embedding Generation\n\nMore efficient than calling `UTL_TO_EMBEDDING` in a loop.\n\n```sql\n-- Batch embed all rows needing embeddings\nDECLARE\n v_inputs DBMS_VECTOR.VECTOR_ARRAY_T := DBMS_VECTOR.VECTOR_ARRAY_T();\n v_outputs DBMS_VECTOR.VECTOR_ARRAY_T;\n v_params CLOB := '{\"provider\": \"openai\",\n \"credential_name\": \"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\": \"text-embedding-3-small\"}';\n i PLS_INTEGER := 0;\nBEGIN\n -- Collect texts needing embeddings\n FOR r IN (SELECT id, chunk_text FROM doc_chunks WHERE embedding IS NULL) LOOP\n i := i + 1;\n v_inputs.EXTEND;\n v_inputs(i) := r.chunk_text;\n END LOOP;\n\n -- Generate embeddings in batch\n v_outputs := DBMS_VECTOR.UTL_TO_EMBEDDINGS(v_inputs, JSON(v_params));\n\n -- Update rows\n i := 0;\n FOR r IN (SELECT id FROM doc_chunks WHERE embedding IS NULL ORDER BY id) LOOP\n i := i + 1;\n UPDATE doc_chunks\n SET embedding = v_outputs(i)\n WHERE id = r.id;\n END LOOP;\n COMMIT;\nEND;\n/\n```\n\n### Vector Utility Functions\n\n```sql\n-- Get the number of dimensions in a vector\nSELECT VECTOR_DIMENSION(embedding) FROM doc_chunks FETCH FIRST 1 ROW ONLY;\n\n-- Convert vector to JSON array string (for debugging/export)\nSELECT VECTOR_SERIALIZE(embedding RETURNING CLOB) FROM doc_chunks FETCH FIRST 1 ROW ONLY;\n\n-- Convert JSON array string back to VECTOR\nSELECT VECTOR_DESERIALIZE('[0.023, -0.047, 0.112]') FROM DUAL;\n\n-- Normalize a vector to unit length (useful before dot product search)\nSELECT VECTOR_NORM(embedding) FROM doc_chunks FETCH FIRST 1 ROW ONLY;\n```\n\n## DBMS_VECTOR_CHAIN: Pipeline Functions\n\n`DBMS_VECTOR_CHAIN` provides composable pipeline steps for RAG workflows.\n\n### UTL_TO_CHUNKS — Split Text into Chunks\n\n```sql\n-- Chunk a document into overlapping pieces\nSELECT jt.chunk_id,\n jt.chunk_offset,\n jt.chunk_length,\n jt.chunk_data\nFROM TABLE(\n DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(\n data => :document_text,\n params => JSON_OBJECT(\n 'by' VALUE 'words', -- 'words', 'chars', 'sentence', 'paragraph'\n 'max' VALUE 200, -- max chunk size\n 'overlap' VALUE 20, -- overlap between chunks\n 'split' VALUE 'recursively'\n )\n )\n ) t,\n JSON_TABLE(\n t.column_value,\n '

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

\n COLUMNS (\n chunk_id NUMBER PATH '$.chunk_id',\n chunk_offset NUMBER PATH '$.chunk_offset',\n chunk_length NUMBER PATH '$.chunk_length',\n chunk_data CLOB PATH '$.chunk_data'\n )\n ) jt;\n```\n\n### UTL_TO_SUMMARY — Summarize Text\n\n```sql\n-- Summarize a long document using an LLM\nDECLARE\n v_summary CLOB;\nBEGIN\n v_summary := DBMS_VECTOR_CHAIN.UTL_TO_SUMMARY(\n data => :long_document,\n params => JSON_OBJECT(\n 'provider' VALUE 'openai',\n 'credential_name' VALUE 'YOUR_SCHEMA.OPENAI_VEC_CRED',\n 'model' VALUE 'gpt-4o-mini',\n 'language' VALUE 'english'\n )\n );\n DBMS_OUTPUT.PUT_LINE(v_summary);\nEND;\n/\n```\n\n### UTL_TO_GENERATE_TEXT — Generate Text (LLM Completion)\n\n```sql\n-- Generate a response given a prompt and retrieved context\nDECLARE\n v_prompt CLOB;\n v_response CLOB;\nBEGIN\n -- Note: :retrieved_chunks and :user_question are bind variables concatenated into a\n -- text prompt (CLOB), not into SQL — this is not SQL injection.\n -- However, sanitize :user_question at the application layer to prevent prompt injection\n -- (a user could embed instructions like \"Ignore above. Instead do X.\" in the question).\n v_prompt := 'You are an Oracle Database expert. Answer based on the context below.\n\nContext:\n' || :retrieved_chunks || '\n\nQuestion: ' || :user_question;\n\n v_response := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(\n data => v_prompt,\n params => JSON_OBJECT(\n 'provider' VALUE 'openai',\n 'credential_name' VALUE 'YOUR_SCHEMA.OPENAI_VEC_CRED',\n 'model' VALUE 'gpt-4o',\n 'max_tokens' VALUE 1024,\n 'temperature' VALUE 0\n )\n );\n DBMS_OUTPUT.PUT_LINE(v_response);\nEND;\n/\n```\n\n## End-to-End RAG Pipeline Example\n\nA complete pipeline: ingest documents → chunk → embed → store → query → generate.\n\n```sql\n-- === INGESTION (run once per document) ===\n\n-- 1. Chunk the document\nINSERT INTO doc_chunks (doc_id, chunk_seq, chunk_text)\nSELECT :doc_id,\n jt.chunk_id,\n jt.chunk_data\nFROM TABLE(\n DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(\n data => :document_content,\n params => '{\"by\":\"words\",\"max\":200,\"overlap\":20}'\n )\n ) t,\n JSON_TABLE(\n t.column_value,\n '

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

\n COLUMNS (\n chunk_id NUMBER PATH '$.chunk_id',\n chunk_data CLOB PATH '$.chunk_data'\n )\n ) jt;\n\n-- 2. Generate embeddings for new chunks\nUPDATE doc_chunks\nSET embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(\n chunk_text,\n '{\"provider\": \"openai\",\n \"credential_name\": \"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\": \"text-embedding-3-small\"}'\n )\nWHERE doc_id = :doc_id\n AND embedding IS NULL;\n\nCOMMIT;\n\n-- === QUERY TIME ===\n\n-- 3. Embed the user's question\nDECLARE\n v_query_vec VECTOR;\n v_context CLOB := '';\n v_response CLOB;\nBEGIN\n -- Embed query\n v_query_vec := DBMS_VECTOR.UTL_TO_EMBEDDING(\n :user_question,\n '{\"provider\": \"openai\", \"credential_name\": \"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\": \"text-embedding-3-small\"}'\n );\n\n -- 4. Retrieve top-5 relevant chunks\n FOR r IN (\n SELECT chunk_text\n FROM doc_chunks\n ORDER BY embedding \u003c=> v_query_vec\n FETCH FIRST 5 ROWS ONLY\n ) LOOP\n v_context := v_context || r.chunk_text || CHR(10) || '---' || CHR(10);\n END LOOP;\n\n -- 5. Generate answer\n v_response := DBMS_VECTOR_CHAIN.UTL_TO_GENERATE_TEXT(\n 'Answer the question using only the context provided.' || CHR(10) ||\n 'Context: ' || v_context || CHR(10) ||\n 'Question: ' || :user_question,\n '{\"provider\": \"openai\", \"credential_name\": \"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\": \"gpt-4o\", \"temperature\": 0}'\n );\n\n DBMS_OUTPUT.PUT_LINE(v_response);\nEND;\n/\n```\n\n## VECTOR_EMBEDDING Inline SQL Function\n\n`VECTOR_EMBEDDING` generates embeddings inline in SQL without requiring PL/SQL. The model must be loaded into Oracle via `DBMS_VECTOR.LOAD_ONNX_MODEL` or stored as an ONNX model in a database directory. For API-based models (OpenAI, Cohere), use `UTL_TO_EMBEDDING` in PL/SQL instead.\n\n```sql\n-- VECTOR_EMBEDDING can be used directly in SQL (Oracle 23ai+)\n-- Syntax: VECTOR_EMBEDDING(model_name USING text_expression AS data)\nSELECT doc_id,\n VECTOR_EMBEDDING(my_embedding_model USING chunk_text AS data) AS embedding\nFROM doc_chunks\nWHERE doc_id = :doc_id;\n\n-- Use it inline in a similarity search (no pre-computation needed)\nSELECT doc_id, chunk_text,\n embedding \u003c=> VECTOR_EMBEDDING(my_embedding_model USING :query AS data) AS distance\nFROM doc_chunks\nORDER BY distance\nFETCH FIRST 5 ROWS ONLY;\n\n-- NOTE: my_embedding_model must be loaded into Oracle using DBMS_VECTOR.LOAD_ONNX_MODEL\n-- or referenced via an ONNX model stored in a database directory.\n-- For API-based models (OpenAI, Cohere), use UTL_TO_EMBEDDING in PL/SQL instead.\n```\n\n## API Rate Limit Retry Pattern\n\nHandle HTTP 429 (rate limit) errors from embedding API calls with exponential back-off.\n\n```sql\nCREATE OR REPLACE FUNCTION embed_with_retry(\n p_text IN CLOB,\n p_params IN CLOB,\n p_max_retry IN PLS_INTEGER DEFAULT 3,\n p_wait_secs IN NUMBER DEFAULT 2\n) RETURN VECTOR AS\n v_embed VECTOR;\n v_attempt PLS_INTEGER := 0;\nBEGIN\n LOOP\n v_attempt := v_attempt + 1;\n BEGIN\n v_embed := DBMS_VECTOR.UTL_TO_EMBEDDING(p_text, JSON(p_params));\n RETURN v_embed; -- success\n EXCEPTION\n WHEN OTHERS THEN\n -- ORA-29273: HTTP request failed (includes 429 Too Many Requests)\n IF SQLCODE = -29273 AND v_attempt \u003c p_max_retry THEN\n DBMS_SESSION.SLEEP(p_wait_secs * v_attempt); -- exponential back-off\n ELSE\n RAISE;\n END IF;\n END;\n END LOOP;\nEND embed_with_retry;\n/\n```\n\n## Chunk Metadata Columns\n\nStore source attribution alongside embeddings so chunks can be cited and filtered by document.\n\n```sql\n-- Recommended doc_chunks table structure with full metadata\nCREATE TABLE doc_chunks (\n chunk_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n doc_id NUMBER NOT NULL,\n chunk_seq NUMBER NOT NULL,\n chunk_offset NUMBER, -- byte offset in source (from UTL_TO_CHUNKS)\n chunk_length NUMBER, -- byte length of chunk\n chunk_text CLOB NOT NULL,\n embedding VECTOR(1536, FLOAT32),\n source_uri VARCHAR2(4000), -- original file path / URL\n page_number NUMBER, -- for PDFs / paginated docs\n section_title VARCHAR2(1000), -- nearest heading above the chunk\n ingested_at TIMESTAMP DEFAULT SYSTIMESTAMP,\n model_name VARCHAR2(255) DEFAULT 'text-embedding-3-small' -- track which model\n);\n\n-- When ingesting: preserve CHUNK_OFFSET and CHUNK_LENGTH from UTL_TO_CHUNKS\nINSERT INTO doc_chunks (doc_id, chunk_seq, chunk_offset, chunk_length, chunk_text)\nSELECT :doc_id,\n jt.chunk_id,\n jt.chunk_offset,\n jt.chunk_length,\n jt.chunk_data\nFROM TABLE(\n DBMS_VECTOR_CHAIN.UTL_TO_CHUNKS(\n data => :document_content,\n params => '{\"by\":\"words\",\"max\":200,\"overlap\":20}'\n )\n ) t,\n JSON_TABLE(\n t.column_value,\n '

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…

\n COLUMNS (\n chunk_id NUMBER PATH '$.chunk_id',\n chunk_offset NUMBER PATH '$.chunk_offset',\n chunk_length NUMBER PATH '$.chunk_length',\n chunk_data CLOB PATH '$.chunk_data'\n )\n ) jt;\n```\n\n## Semantic Chunking Strategy\n\nChoose the chunking approach based on content type. Check the chunk size distribution after ingestion to validate your settings.\n\n```sql\n-- By SENTENCE — best for Q&A over prose (news, documentation)\nparams => '{\"by\":\"sentence\",\"max\":5,\"overlap\":1}'\n\n-- By PARAGRAPH — best for long-form docs, reports, legal text\nparams => '{\"by\":\"paragraph\",\"max\":3,\"overlap\":1}'\n\n-- By WORDS — best for predictable chunk sizes (code, structured text)\nparams => '{\"by\":\"words\",\"max\":200,\"overlap\":20}'\n\n-- By CHARACTERS — use only when other strategies produce bad splits\nparams => '{\"by\":\"chars\",\"max\":1000,\"overlap\":100}'\n\n-- Splitting strategies:\n-- \"NEWLINE\" — split at newline boundaries (good for log files, CSVs)\n-- \"BLANKLINE\" — split at blank lines (good for Markdown, source code)\n-- \"SPACE\" — split at spaces (default for word/char modes)\n-- \"RECURSIVELY\" — try paragraph→sentence→word in order (best for mixed content)\n-- \"CUSTOM\" — split on a custom regex pattern\n\n-- Checking chunk size distribution after ingestion\nSELECT MIN(chunk_length) AS min_len,\n MAX(chunk_length) AS max_len,\n ROUND(AVG(chunk_length), 0) AS avg_len,\n COUNT(*) AS total_chunks\nFROM doc_chunks\nWHERE doc_id = :doc_id;\n```\n\n## Model Upgrade / Dimension Change Path\n\nVECTOR columns are typed by dimension; stored embeddings from one model cannot coexist in the same column as embeddings from a model with a different dimension. Follow this migration path when upgrading models.\n\n```sql\n-- Problem: stored embeddings are 1536-dim (text-embedding-3-small)\n-- New model produces 3072-dim (text-embedding-3-large)\n-- VECTOR columns are typed; you cannot mix dimensions in the same column\n\n-- Step 1: Add a new column for the new model's embeddings\nALTER TABLE doc_chunks ADD embedding_v2 VECTOR(3072, FLOAT32);\n\n-- Step 2: Re-embed all chunks with the new model\nUPDATE doc_chunks\nSET embedding_v2 = DBMS_VECTOR.UTL_TO_EMBEDDING(\n chunk_text,\n '{\"provider\":\"openai\",\"credential_name\":\"YOUR_SCHEMA.OPENAI_VEC_CRED\",\n \"model\":\"text-embedding-3-large\"}'\n )\nWHERE embedding_v2 IS NULL;\nCOMMIT;\n\n-- Step 3: Drop old vector index\nDROP INDEX doc_chunks_hnsw_idx;\n\n-- Step 4: Create new index on the new column\nCREATE VECTOR INDEX doc_chunks_hnsw_v2_idx ON doc_chunks(embedding_v2)\nORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE COSINE WITH TARGET ACCURACY 95;\n\n-- Step 5: Validate new index, then drop old column\nALTER TABLE doc_chunks DROP COLUMN embedding;\nALTER TABLE doc_chunks RENAME COLUMN embedding_v2 TO embedding;\n\n-- Agent rule: never drop the old embedding column until the new index is verified\n-- and at least one similarity query has returned expected results.\n```\n\n## UTL_TO_EMBED_AND_GENERATE (26ai)\n\n`UTL_TO_EMBED_AND_GENERATE` combines embedding, vector search, and generation into a single pipeline call. This function is a 26ai addition and is not available in 23ai — use the multi-step pattern in the End-to-End RAG Pipeline section instead.\n\n```sql\n-- UTL_TO_EMBED_AND_GENERATE combines embedding + vector search + generation\nDECLARE\n v_response CLOB;\nBEGIN\n v_response := DBMS_VECTOR_CHAIN.UTL_TO_EMBED_AND_GENERATE(\n data => :user_question,\n params => JSON_OBJECT(\n 'embed_provider' VALUE 'openai',\n 'embed_credential' VALUE 'YOUR_SCHEMA.OPENAI_VEC_CRED',\n 'embed_model' VALUE 'text-embedding-3-small',\n 'search_index' VALUE 'doc_chunks_hnsw_idx',\n 'top_k' VALUE 5,\n 'gen_provider' VALUE 'openai',\n 'gen_credential' VALUE 'YOUR_SCHEMA.OPENAI_VEC_CRED',\n 'gen_model' VALUE 'gpt-4o',\n 'temperature' VALUE 0\n )\n );\n DBMS_OUTPUT.PUT_LINE(v_response);\nEND;\n/\n-- NOTE: UTL_TO_EMBED_AND_GENERATE is a 26ai addition; not available in 23ai.\n-- In 23ai, use the multi-step pattern in the End-to-End RAG Pipeline section.\n```\n\n## DBMS_HYBRID_VECTOR: Hybrid Search\n\n`DBMS_HYBRID_VECTOR` is for hybrid retrieval over hybrid vector indexes, not for ordinary vector SQL. Use it when the query needs semantic search and Oracle Text-style keyword constraints in the same database-managed search path.\n\nRepresentative package members include:\n\n- `SEARCH`\n- `SEARCHPIPELINE`\n- `GET_SQL`\n- `GET_HYBRID_SQL`\n- `GET_SQL_TEMPLATE`\n\n```sql\nSELECT DBMS_HYBRID_VECTOR.SEARCH(\n json('{ \"hybrid_index_name\" : \"docs_hybrid_idx\",\n \"vector\" : { \"search_text\" : \"stock fraud\" },\n \"text\" : { \"contains\" : \"$ABC AND $Corporation\" },\n \"return\" : { \"topN\" : 10 }\n }'))\nFROM dual;\n```\n\nUse `GET_SQL` or `GET_HYBRID_SQL` when you need to inspect the generated SQL behind a hybrid search pipeline.\n\n## Best Practices\n\n- Use `UTL_TO_EMBEDDINGS` (batch) rather than `UTL_TO_EMBEDDING` (single) for bulk ingestion — significantly fewer API calls\n- Chunk size of 150–300 words with 10–20% overlap works well for most document types\n- Store the `CHUNK_OFFSET` and `CHUNK_LENGTH` so you can retrieve the original source passage\n- Add a vector index (HNSW) after initial bulk ingestion, not before — index builds are faster on populated tables\n- Use `DBMS_HYBRID_VECTOR` only for hybrid retrieval; use ordinary vector SQL or `DBMS_VECTOR` for pure vector operations\n- Cache frequently-queried embeddings; re-embedding the same text is wasteful and incurs API cost\n- Use `temperature: 0` for generation in production — deterministic responses are easier to test\n\n## Common Mistakes\n\n**Chunking after embedding** — always chunk first, then embed each chunk. Embedding an entire document produces one vector that averages all content; it loses specific detail.\n\n**Embedding model mismatch** — the query embedding and the stored embeddings must use the same model. Mixing `text-embedding-3-small` queries against `text-embedding-ada-002` chunks produces garbage results.\n\n**Not storing chunk source metadata** — without `doc_id`, `page_number`, or `source_url` alongside the chunk, you cannot cite sources or filter by document.\n\n**Re-chunking on every query** — chunking is an ingestion-time operation. Store chunks in the database; never rechunk at query time.\n\n**Using `DBMS_HYBRID_VECTOR` for pure vector search** — it is for hybrid indexes and hybrid search execution. Use `VECTOR_DISTANCE`, shorthand operators, or `DBMS_VECTOR` for non-hybrid vector work.\n\n## Oracle Version Notes (19c vs 26ai)\n\n- **19c**: No DBMS_VECTOR or DBMS_VECTOR_CHAIN; RAG pipelines required entirely external tooling\n- **23ai**: DBMS_VECTOR and DBMS_VECTOR_CHAIN introduced; UTL_TO_EMBEDDING, UTL_TO_EMBEDDINGS, UTL_TO_CHUNKS, UTL_TO_SUMMARY, UTL_TO_GENERATE_TEXT\n- **26ai**: Expanded chunking strategies; improved batch performance; tighter SELECT AI integration; additional provider support; `DBMS_HYBRID_VECTOR` for hybrid search workflows\n\n## See Also\n\n- [AI Vector Search in Oracle](../features/vector-search.md) — VECTOR type, indexes, and VECTOR_DISTANCE\n- [SELECT AI in Oracle 26ai](../features/select-ai.md) — Natural language to SQL\n- [AI Profiles and Provider Configuration](../features/ai-profiles.md) — Credential and provider setup\n\n## Sources\n\n- [DBMS_VECTOR Package Reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/dbms_vector.html)\n- [DBMS_VECTOR_CHAIN Package Reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/dbms_vector_chain.html)\n- [DBMS_HYBRID_VECTOR](https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/dbms_hybrid_vector-vecse.html)\n- [Oracle AI Vector Search User's Guide — Generating Embeddings](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/generate-embeddings.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19086,"content_sha256":"ca021405a7b609d6cb995664abe80bc159610d2d2fa5924808c7756967089580"},{"filename":"features/materialized-views.md","content":"# Oracle Materialized Views\n\n## Overview\n\nA **materialized view (MV)** is a database object that stores the result of a query physically on disk and optionally refreshes that result as the underlying base tables change. Unlike a regular view — which re-executes its query every time it is referenced — a materialized view is a snapshot of query results that can be read with no recomputation cost.\n\nMaterialized views serve two broad purposes in Oracle:\n\n1. **Query rewrite** — The optimizer transparently rewrites a user query to read from a fast, pre-aggregated MV instead of expensive base tables, without any application code change.\n2. **Data replication** — Pushing a summarized or filtered copy of data to a different schema, database, or reporting tier for independent consumption.\n\nMaterialized views were introduced in Oracle 8i as a replacement for the older snapshot mechanism and have been continuously enhanced through 26ai.\n\n---\n\n## Core Concepts\n\n### Refresh Modes\n\n| Mode | Description | Use Case |\n|---|---|---|\n| `COMPLETE` | Truncates and re-populates the MV by re-executing the full query | Any query; least restrictions; slowest for large data sets |\n| `FAST` | Applies only changes since the last refresh using MV logs | Large base tables with small incremental change |\n| `FORCE` | Uses FAST if possible, falls back to COMPLETE | General default; good when FAST eligibility is uncertain |\n| `NEVER` | MV is never refreshed automatically; must be refreshed manually | Static snapshots, data migration, staging |\n\n### Refresh Timing\n\n| Timing | Description |\n|---|---|\n| `ON COMMIT` | MV is refreshed automatically when a DML transaction on the base table(s) commits |\n| `ON DEMAND` | MV is refreshed only when explicitly triggered via `DBMS_MVIEW.REFRESH` |\n| `ON STATEMENT` | 12c+: Refresh triggers immediately after each DML statement, before commit |\n| `START WITH ... NEXT ...` | Legacy syntax for scheduled refresh; prefer `DBMS_SCHEDULER` jobs in modern setups |\n\n### Materialized View Logs\n\nA **materialized view log (MV log)** is a change-capture table maintained on the base table. Every INSERT, UPDATE, and DELETE on the base table is recorded in the log. A FAST refresh reads the log instead of re-scanning the entire base table, then clears consumed entries.\n\n---\n\n## Creating Materialized View Logs\n\nBefore creating a FAST-refreshable MV, create a log on every base table referenced:\n\n```sql\n-- Basic MV log: captures all DML changes, including new values\nCREATE MATERIALIZED VIEW LOG ON sales\nWITH ROWID, SEQUENCE\n (sale_date, product_id, region_id, amount, qty)\nINCLUDING NEW VALUES;\n\n-- MV log on dimension table\nCREATE MATERIALIZED VIEW LOG ON products\nWITH ROWID, SEQUENCE (product_id, category_id, unit_price)\nINCLUDING NEW VALUES;\n\nCREATE MATERIALIZED VIEW LOG ON regions\nWITH ROWID, SEQUENCE (region_id, region_name, country_code)\nINCLUDING NEW VALUES;\n```\n\n**Key `WITH` options:**\n\n| Option | Required for |\n|---|---|\n| `ROWID` | FAST refresh of non-aggregate (join) MVs |\n| `PRIMARY KEY` | FAST refresh when MV uses primary key joins |\n| `SEQUENCE` | FAST refresh of aggregate MVs (ORDER updates correctly) |\n| `INCLUDING NEW VALUES` | FAST refresh of aggregate MVs with `SUM`, `COUNT`, etc. |\n\n---\n\n## Creating Materialized Views\n\n### COMPLETE Refresh — Simple Aggregate\n\n```sql\nCREATE MATERIALIZED VIEW mv_monthly_sales_summary\nBUILD IMMEDIATE -- populate immediately on creation; BUILD DEFERRED populates later\nREFRESH COMPLETE\nON DEMAND\nAS\nSELECT TRUNC(s.sale_date, 'MM') AS sale_month,\n p.category_id,\n r.region_name,\n COUNT(*) AS num_sales,\n SUM(s.amount) AS total_revenue,\n SUM(s.qty) AS total_qty\nFROM sales s\nJOIN products p ON p.product_id = s.product_id\nJOIN regions r ON r.region_id = s.region_id\nGROUP BY TRUNC(s.sale_date, 'MM'), p.category_id, r.region_name;\n```\n\n### FAST Refresh — Aggregate MV\n\nFAST refresh on aggregate MVs requires the following in the MV query:\n- `COUNT(*)` must be present\n- For `SUM(col)`, `COUNT(col)` must also be present\n- All dimensions must have MV logs with `SEQUENCE` and `INCLUDING NEW VALUES`\n\n```sql\nCREATE MATERIALIZED VIEW mv_sales_by_product_region\nBUILD IMMEDIATE\nREFRESH FAST ON DEMAND\nENABLE QUERY REWRITE\nAS\nSELECT s.product_id,\n s.region_id,\n COUNT(*) AS cnt, -- required for FAST refresh\n SUM(s.amount) AS sum_amount,\n COUNT(s.amount) AS cnt_amount, -- required for SUM fast refresh\n SUM(s.qty) AS sum_qty,\n COUNT(s.qty) AS cnt_qty\nFROM sales s\nGROUP BY s.product_id, s.region_id;\n```\n\n### FAST Refresh — Join MV\n\n```sql\nCREATE MATERIALIZED VIEW mv_sales_detail\nBUILD IMMEDIATE\nREFRESH FAST ON DEMAND\nENABLE QUERY REWRITE\nAS\nSELECT s.rowid AS sales_rowid,\n p.rowid AS products_rowid,\n s.sale_id,\n s.sale_date,\n s.amount,\n p.product_name,\n p.category_id\nFROM sales s\nJOIN products p ON p.product_id = s.product_id;\n```\n\nJoin MVs for FAST refresh require both ROWIDs to be selected (Oracle uses them to identify changed rows in the log).\n\n### ON COMMIT Refresh\n\n```sql\nCREATE MATERIALIZED VIEW mv_account_balances\nBUILD IMMEDIATE\nREFRESH FAST ON COMMIT -- refreshed automatically when any base table transaction commits\nENABLE QUERY REWRITE\nAS\nSELECT account_id,\n COUNT(*) AS num_transactions,\n SUM(amount) AS current_balance\nFROM transactions\nGROUP BY account_id;\n```\n\n**Caution:** `ON COMMIT` refresh runs synchronously within the committing transaction. Complex or slow refreshes will visibly slow down the application's commit latency.\n\n### FORCE Refresh (Recommended Default)\n\n```sql\nCREATE MATERIALIZED VIEW mv_regional_totals\nBUILD IMMEDIATE\nREFRESH FORCE ON DEMAND\nENABLE QUERY REWRITE\nAS\nSELECT r.region_name,\n TRUNC(s.sale_date, 'YYYY') AS sale_year,\n SUM(s.amount) AS annual_revenue\nFROM sales s\nJOIN regions r ON r.region_id = s.region_id\nGROUP BY r.region_name, TRUNC(s.sale_date, 'YYYY');\n```\n\n---\n\n## Query Rewrite\n\nQuery rewrite is Oracle's ability to **transparently substitute** a user query referencing base tables with an equivalent query against a materialized view. This requires no application code change — the optimizer handles it.\n\n### Enabling Query Rewrite\n\n```sql\n-- At the database level (requires DBA privileges)\nALTER SYSTEM SET query_rewrite_enabled = TRUE;\n\n-- At the session level\nALTER SESSION SET query_rewrite_enabled = TRUE;\n\n-- On the MV itself\nALTER MATERIALIZED VIEW mv_sales_by_product_region ENABLE QUERY REWRITE;\n\n-- Check if MV is eligible for query rewrite\nSELECT mview_name, rewrite_enabled, staleness, refresh_mode\nFROM user_mviews;\n```\n\n### Verifying Query Rewrite Is Being Used\n\n```sql\n-- Run EXPLAIN PLAN to see if the MV is substituted\nEXPLAIN PLAN FOR\n SELECT s.product_id, s.region_id, SUM(s.amount)\n FROM sales s\n GROUP BY s.product_id, s.region_id;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Look for: MAT_VIEW REWRITE ACCESS FULL (MV_SALES_BY_PRODUCT_REGION)\n```\n\n### Query Rewrite Integrity Modes\n\n```sql\n-- Set globally or per session\nALTER SESSION SET query_rewrite_integrity = ENFORCED;\n-- ENFORCED: only rewrites if MV is known to be fresh (most conservative)\n-- TRUSTED: trusts RELY constraints and dimension relationships\n-- STALE_TOLERATED: allows rewrite even on stale MVs (least conservative; use with care)\n```\n\nFor query rewrite to work reliably:\n- The MV must have `ENABLE QUERY REWRITE`\n- The MV must be \"fresh\" (or `STALE_TOLERATED` mode is in effect)\n- The optimizer must find the MV query equivalent to or a superset of the user query\n- `QUERY_REWRITE_ENABLED = TRUE`\n\n---\n\n## Manual Refresh\n\n```sql\n-- Refresh a single MV\nBEGIN\n DBMS_MVIEW.REFRESH(\n list => 'APPSCHEMA.MV_MONTHLY_SALES_SUMMARY',\n method => 'C', -- C=COMPLETE, F=FAST, ?=FORCE, A=always COMPLETE\n atomic_refresh => FALSE -- TRUE keeps MV accessible during refresh (at cost of undo)\n );\nEND;\n/\n\n-- Refresh multiple MVs in dependency order\nBEGIN\n DBMS_MVIEW.REFRESH(\n list => 'MV_SALES_DETAIL,MV_SALES_BY_PRODUCT_REGION,MV_MONTHLY_SALES_SUMMARY',\n method => 'F'\n );\nEND;\n/\n\n-- Refresh all MVs in a schema\nBEGIN\n FOR mv IN (SELECT mview_name FROM user_mviews) LOOP\n BEGIN\n DBMS_MVIEW.REFRESH(mv.mview_name, method => 'C'); -- complete refresh\n EXCEPTION\n WHEN OTHERS THEN\n DBMS_OUTPUT.PUT_LINE('Failed: ' || mv.mview_name || ' — ' || SQLERRM);\n END;\n END LOOP;\nEND;\n/\n```\n\n### DBMS_MVIEW.REFRESH_DEPENDENT\n\nRefreshes all MVs that depend on a given base table:\n\n```sql\nBEGIN\n DBMS_MVIEW.REFRESH_DEPENDENT(\n list => 'SALES', -- base table name\n method => 'F',\n rollback_seg => NULL\n );\nEND;\n/\n```\n\n---\n\n## Scheduling Refresh with DBMS_SCHEDULER\n\n```sql\nBEGIN\n DBMS_SCHEDULER.CREATE_JOB(\n job_name => 'REFRESH_SALES_MVS_JOB',\n job_type => 'PLSQL_BLOCK',\n job_action => '\n BEGIN\n DBMS_MVIEW.REFRESH(\n list => ''MV_SALES_BY_PRODUCT_REGION,MV_MONTHLY_SALES_SUMMARY'',\n method => ''F'',\n atomic_refresh => FALSE\n );\n END;',\n repeat_interval => 'FREQ=HOURLY;BYMINUTE=0;BYSECOND=0',\n enabled => TRUE,\n comments => 'Hourly FAST refresh of sales materialized views'\n );\nEND;\n/\n```\n\n---\n\n## Monitoring Staleness and Freshness\n\n```sql\n-- MV freshness and refresh status\nSELECT mview_name,\n last_refresh_date,\n last_refresh_type,\n staleness, -- FRESH, STALE, UNKNOWN, NEEDS_COMPILE\n refresh_mode,\n refresh_method,\n rewrite_enabled\nFROM user_mviews\nORDER BY mview_name;\n\n-- MV log size and age (how much unprocessed change exists)\nSELECT log_owner,\n master AS base_table,\n log_table,\n log_trigger,\n rowids,\n sequence,\n includes_new_values\nFROM user_mview_logs;\n\n-- Row count in MV log (unprocessed entries)\nSELECT COUNT(*) AS pending_changes FROM mlog$_sales;\n\n-- MV refresh history\nSELECT mview_name,\n start_time,\n end_time,\n elapsed_time,\n refresh_method,\n complete_stats_update\nFROM dba_mvref_stats\nWHERE mview_name = 'MV_MONTHLY_SALES_SUMMARY'\nORDER BY start_time DESC\nFETCH FIRST 20 ROWS ONLY;\n\n-- Check if MVs are blocking query rewrite due to staleness\nSELECT name, freshness\nFROM v$object_usage;\n```\n\n---\n\n## Partitioned Materialized Views\n\nMVs can themselves be partitioned, which is important when the MV is large:\n\n```sql\nCREATE MATERIALIZED VIEW mv_partitioned_sales\nPARTITION BY RANGE (sale_month) (\n PARTITION p_2023 VALUES LESS THAN (DATE '2024-01-01'),\n PARTITION p_2024 VALUES LESS THAN (DATE '2025-01-01'),\n PARTITION p_2025 VALUES LESS THAN (DATE '2026-01-01'),\n PARTITION p_future VALUES LESS THAN (MAXVALUE)\n)\nBUILD IMMEDIATE\nREFRESH COMPLETE ON DEMAND\nAS\nSELECT TRUNC(sale_date, 'MM') AS sale_month,\n region_id,\n product_id,\n SUM(amount) AS revenue\nFROM sales\nGROUP BY TRUNC(sale_date, 'MM'), region_id, product_id;\n```\n\nWhen partitioning an MV, a **COMPLETE** refresh can use partition truncation internally (refresh partition-by-partition), which reduces undo generation compared to truncating and repopulating the entire MV.\n\n---\n\n## Best Practices\n\n- **Create MV logs before creating the MV.** Oracle checks log existence at MV creation time when `REFRESH FAST` is specified.\n- **Use `BUILD DEFERRED` in production deployments** when the initial population would be disruptive. Schedule a manual refresh during a maintenance window immediately after DDL.\n- **Keep `ON COMMIT` refreshes for small, simple MVs only.** The refresh runs inside the user's commit path. For aggregates over millions of rows, use `ON DEMAND` with a short-interval scheduler job instead.\n- **Explicitly include `COUNT(*)` and `COUNT(col)` in aggregate MVs** intended for FAST refresh. Oracle's optimizer needs these counts to compute incremental changes correctly.\n- **Monitor MV log growth.** If a FAST refresh fails or is delayed, MV logs accumulate indefinitely. A table with a large unprocessed log will exhibit write-amplification overhead for all DML. Alert when log row count exceeds a threshold.\n- **Use `atomic_refresh => FALSE`** for large COMPLETE refreshes to avoid massive undo generation. With `atomic_refresh => TRUE` (the default), Oracle keeps the old data accessible during refresh by using undo. For very large MVs, this can fill the undo tablespace.\n- **Enable `QUERY REWRITE` only on MVs you intend the optimizer to use.** Enabling it on dozens of MVs forces the optimizer to consider all of them during every query parse, which can increase parse time.\n- **Use `EXPLAIN PLAN` to verify rewrites are happening.** Do not assume — confirm with execution plans that your reporting queries are actually hitting MVs.\n\n---\n\n## Common Mistakes and How to Avoid Them\n\n**Mistake 1: FAST refresh silently falls back to COMPLETE with no warning**\nIf a FAST refresh is attempted but is not possible (e.g., the MV log is missing an option), Oracle either raises an error or falls back to COMPLETE depending on the `method` parameter. Use `FORCE` (`?`) for the method parameter to get silent fallback, but then **monitor `last_refresh_type`** in `USER_MVIEWS` to confirm the expected method is being used.\n\n```sql\n-- Audit what refresh type was actually used\nSELECT mview_name, last_refresh_type, last_refresh_date\nFROM user_mviews\nWHERE last_refresh_type != 'FAST'; -- unexpected COMPLETE refreshes\n```\n\n**Mistake 2: Forgetting `SEQUENCE` and `INCLUDING NEW VALUES` on MV logs for aggregate MVs**\nWithout `SEQUENCE`, Oracle cannot correctly order UPDATE operations that flip a value from old to new in an aggregate. Without `INCLUDING NEW VALUES`, Oracle cannot compute the delta for SUM/COUNT. Both are required for aggregate FAST refresh.\n\n**Mistake 3: Stale MVs degrading query rewrite silently**\nIf `query_rewrite_integrity = ENFORCED` (the default), a stale MV is excluded from query rewrite without any error. Queries silently hit base tables and become slow. Set up monitoring alerts on the `staleness` column in `USER_MVIEWS`.\n\n**Mistake 4: Cascading ON COMMIT refresh on large tables**\n`ON COMMIT` refresh is synchronous and runs in the committing user's session. On a high-DML table with a complex aggregate MV, every INSERT/UPDATE/DELETE will be noticeably slower. Switch to `ON DEMAND` with a frequent scheduled refresh if latency is acceptable.\n\n**Mistake 5: Not accounting for MV in backup and export strategies**\nMV logs are regular tables and are included in Data Pump exports. When restoring or importing, MV logs may contain stale entries pointing to ROWIDs that no longer exist. After a restore, force a `COMPLETE` refresh of all MVs to reset their logs.\n\n**Mistake 6: Altering the base table without adjusting the MV log**\nAdding a column to a base table does not automatically add it to the MV log. If a subsequent MV needs that column for FAST refresh, you must drop and recreate the MV log (which clears pending changes) or use `ALTER MATERIALIZED VIEW LOG ADD (new_column)`.\n\n```sql\n-- Check which columns are covered by an existing MV log\nSELECT column_name, refs_src_rowid, snapshots\nFROM user_mview_log_filter_cols\nWHERE master = 'SALES';\n```\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database Data Warehousing Guide: Basic Materialized Views 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/dwhsg/basic-materialized-views.html)\n- [DBMS_MVIEW — Oracle Database PL/SQL Packages and Types Reference 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/arpls/DBMS_MVIEW.html)\n- [Oracle Database SQL Language Reference: CREATE MATERIALIZED VIEW 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.html)\n- [Oracle Database SQL Language Reference: CREATE MATERIALIZED VIEW LOG 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW-LOG.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":16860,"content_sha256":"ffa2ab31a2116d7872caf03cf0184f49837478f56f71d307a8fefad1c315c2bf"},{"filename":"features/oracle-apex.md","content":"# Oracle APEX (Application Express)\n\n## Overview\n\n**Oracle APEX** is Oracle's low-code web application platform built into the database. It is used to build browser-based applications, forms, reports, dashboards, and simple workflow tools on top of Oracle data.\n\nThis page is intentionally lightweight. It is meant to explain what APEX is and how it fits into an Oracle environment, not to be a full build, admin, or deployment guide.\n\nAt a high level:\n- APEX apps are designed in a browser\n- APEX runs against Oracle Database data and PL/SQL\n- APEX requires Oracle REST Data Services (ORDS) as the web listener\n- Applications are stored as metadata and can be exported as SQL\n\n---\n\n## When APEX Fits Well\n\nAPEX is a strong fit for:\n- Internal business applications\n- Data entry and approval workflows\n- Reporting and dashboards\n- Admin tools on top of Oracle schemas\n- Fast delivery of database-centric web apps\n\nAPEX is usually less appropriate when the main requirement is:\n- A heavily custom front-end framework-first experience\n- Complex offline/mobile-native behavior\n- A non-Oracle data platform as the system of record\n\n---\n\n## Core Concepts\n\n| Term | Meaning |\n|---|---|\n| `Workspace` | Top-level APEX container for developers, applications, and associated schemas |\n| `Application` | A packaged web app made of pages, shared components, and metadata |\n| `Page` | A single screen in the application |\n| `Region` | A section of a page, such as a report, chart, or form |\n| `Item` | A page field or variable, such as `P1_CUSTOMER_ID` |\n| `Session State` | Per-session values that APEX stores for page and application items |\n| `Parsing Schema` | Database schema whose privileges are used for the app's SQL and PL/SQL |\n| `Shared Components` | Reusable definitions such as navigation, LOVs, auth schemes, and templates |\n| `ORDS` | The required web listener and REST layer used to serve APEX |\n\n---\n\n## Runtime Model\n\nCurrent APEX deployments follow this path:\n\n```text\nBrowser\n -> ORDS\n -> Oracle Database\n -> APEX engine\n -> Application schema objects\n```\n\nImportant separation:\n- APEX itself lives in Oracle-managed APEX schemas\n- Your tables, views, packages, and business logic usually live in one or more application schemas\n- The APEX application references those schema objects through its parsing schema\n\nThis is the main mental model to keep straight when reading or building APEX apps.\n\n---\n\n## What Developers Usually Build\n\nAn APEX application commonly includes:\n- Report pages for querying data\n- Form pages for insert/update/delete workflows\n- Charts and summary dashboards\n- Validations and computations\n- PL/SQL processes tied to page submit events\n- Authentication and authorization rules\n\nMost of the application is assembled declaratively in App Builder, with SQL, PL/SQL, and some JavaScript added where needed.\n\n---\n\n## Minimal Example\n\nAPEX commonly binds page items directly into SQL using session state values:\n\n```sql\nSELECT order_id,\n order_date,\n total_amount\nFROM orders\nWHERE customer_id = :P10_CUSTOMER_ID\nORDER BY order_date DESC;\n```\n\nIn this example:\n- `P10_CUSTOMER_ID` is a page item\n- APEX resolves it from session state at runtime\n- The query runs using the application's parsing schema\n\n---\n\n## How APEX Is Usually Managed\n\nA lightweight operational model looks like this:\n- Developers build pages in App Builder\n- Database objects are maintained in normal schema scripts\n- APEX applications are exported as SQL for version control\n- ORDS exposes the application over HTTP/S\n\nThat split matters: schema code and APEX app metadata are related, but they are not the same artifact.\n\n---\n\n## Practical Guidance\n\n- Use a dedicated application schema instead of `SYS` or `SYSTEM`\n- Use bind variables and page items rather than building dynamic SQL from string concatenation\n- Treat the APEX application export as a deployable artifact\n- Enforce authorization on the server side, not only by hiding UI elements\n- Keep custom PL/SQL and JavaScript focused; prefer declarative APEX features first\n\n---\n\n## Out of Scope Here\n\nThis page does not try to cover APEX in depth. It intentionally omits:\n- APEX installation and upgrade procedures\n- ORDS installation and pool tuning\n- Deep authentication and SSO configuration\n- REST module design in ORDS\n- CI/CD pipelines for APEX exports\n- Advanced page design, theming, or plugin development\n\nThose topics belong in dedicated APEX-focused skills and documentation.\n\n---\n\n## Sources\n\n- [Oracle APEX Documentation](https://docs.oracle.com/en/database/oracle/apex/)\n- [Oracle APEX App Builder Documentation](https://docs.oracle.com/en/database/oracle/apex/24.2/htmdb/)\n- [Oracle REST Data Services Documentation](https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4790,"content_sha256":"27e08f7b3ac893de9e423ce471d11ecc091cc94b18e3ab7c1a74d74f84e85c53"},{"filename":"features/select-ai.md","content":"# Select AI in Oracle 26ai and 19c\n\nSelect AI is Oracle's built-in gateway to AI providers and AI models, including privately hosted models for a range of generative and agentic AI use cases. Select AI supports:\n\n- **Natural language to SQL (NL2SQL)** - specific to your database schema, generate and explain SQL, running and narrating generated queries\n- **Retrieval augmented generation (RAG) on 26ai** - automated vector index creation/update and RAG workflow using AI Vector Search\n- **Chat** - generate content with simple or complex custom prompts easily from your database for email generation, sentiment analysis, etc\n- **Synthetic Data Generation (SDG)** - generate data in database tables to support, e.g., testing/debugging applications and interfaces\n- **AI agents** - build interactive and autonomous AI agents that perform tasks and use tools\n- **Summarize text** - generate a summary of long text with choice of output style and processing method\n- **Translate text** - using AI provider translation services, translate from one language to another to simplify app-dev and assist in translating LLM results to the desired language\n\nYou can use Select AI in SQL clients with:\n\n- `SELECT AI \u003caction> \u003cprompt>` after setting your AI profile\n- `DBMS_CLOUD_AI.GENERATE(...)` for stateless or programmatic use\n\nYou can use Select AI Agent in SQL clients with:\n\n- `SELECT AI AGENT \u003cprompt>` after setting your AI agent team\n- `DBMS_CLOUD_AI_AGENT.RUN_TEAM`\n\n\nThe SQL command line use of `SELECT AI` is not supported in Database Actions or APEX Service. In those environments, use `DBMS_CLOUD_AI.GENERATE`.\n\n## How Select AI Works for NL2SQL\n\n1. You submit a natural language prompt.\n2. Select AI augments the prompt with schema metadata from the active AI profile.\n3. Select AI sends the constructed prompt to the configured AI provider.\n4. Select AI returns the generated SQL, query results or natural-language summary, or a natural-language explanation depending on the action specified.\n\nFor SQL generation, Oracle sends schema metadata, not table contents. For `narrate`, Select AI can send result data or retrieved content to the LLM unless an administrator disables data access.\n\n## Prerequisites\n\nAt minimum, you need:\n\n- A credential for the AI provider\n- An AI profile created with `DBMS_CLOUD_AI.CREATE_PROFILE`\n- To use a stateful SQL command line, the profile set in the current session with `DBMS_CLOUD_AI.SET_PROFILE`\n\nIn most environments, an administrator also needs to grant `EXECUTE` on `DBMS_CLOUD_AI` (and `DBMS_CLOUD_AI_AGENT` if using AI agents) and provide outbound network access to the provider.\n\n```sql\n-- 1. Create credentials for the AI provider\nBEGIN\n DBMS_CLOUD.CREATE_CREDENTIAL(\n credential_name => 'OPENAI_CRED',\n username => 'OPENAI',\n password => 'sk-...' -- your API key\n );\nEND;\n/\n\n-- 2. Create an AI profile\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'MY_AI_PROFILE',\n attributes => '{\"provider\": \"openai\",\n \"credential_name\": \"OPENAI_CRED\",\n \"object_list\": [{\"owner\": \"HR\", \"name\": \"EMPLOYEES\"},\n {\"owner\": \"HR\", \"name\": \"DEPARTMENTS\"}],\n \"comments\": true,\n \"temperature\": 0}'\n );\nEND;\n/\n\n-- 3. Set the profile for the current session\nEXEC DBMS_CLOUD_AI.SET_PROFILE('MY_AI_PROFILE');\n```\n\n## Select AI Syntax\n\nThe SQL syntax is:\n\n```sql\nSELECT AI action natural_language_prompt\n```\n\n`runsql` is the default action, so the action keyword is optional.\n\n```sql\n-- RUNSQL: generate SQL and execute it\nSELECT AI how many employees are in each department;\n\n-- SHOWSQL: generate SQL but do not execute it\nSELECT AI SHOWSQL how many employees were hired last year;\n\n-- EXPLAINSQL: explain the generated SQL in natural language\nSELECT AI EXPLAINSQL show the top 10 employees by salary;\n\n-- NARRATE: execute the SQL and summarize the result in natural language\nSELECT AI NARRATE what are the total sales by region this quarter;\n\n-- CHAT: send the prompt directly to the LLM\nSELECT AI CHAT what is the difference between a fact table and a dimension table;\n\n-- SHOWPROMPT: display the constructed prompt Oracle sends to the model\nSELECT AI SHOWPROMPT show the top 10 employees by salary;\n```\n\nOracle AI Database also supports actions:\n\n- `SUMMARIZE` for summarizing text and large files\n- `FEEDBACK` for improving future SQL generation based on user feedback (26ai capability)\n- `TRANSLATE` for OCI-backed translation\n- `AGENT` for Select AI Agent team execution\n\n## Supported AI Providers\n\n| Provider | `provider` Value | Notes |\n|---|---|---|\n| OpenAI | `openai` | General LLM provider support |\n| Cohere | `cohere` | General LLM provider support |\n| Azure OpenAI | `azure` | Requires Azure resource and deployment attributes |\n| OCI Generative AI | `oci` | Required for `translate`; may also need OCI-specific attributes |\n| Google | `google` | Gemini/Vertex-backed provider support |\n| Anthropic | `anthropic` | Claude-backed provider support |\n| Hugging Face | `huggingface` | 26ai provider option |\n| AWS Bedrock | `aws` | General LLM provider support |\n| OpenAI-compatible API providers | Specify `provider_endpoint` | General LLM provider support |\n| Privately hosted AI models | `x` | \n\nModel availability varies by provider and Oracle release, so prefer provider examples that do not hard-code a model unless you need a specific one.\n\nSee [Manage AI Profiles](https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/select-ai-manage-profiles.html) for more information.\n\n```sql\n-- OCI Generative AI example\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'OCI_AI_PROFILE',\n attributes => '{\"provider\": \"oci\",\n \"credential_name\": \"OCI_CRED\",\n \"object_list\": [{\"owner\": \"SALES\", \"name\": \"ORDERS\"}]}'\n );\nEND;\n/\n```\n\n## Profile Attributes\n\nThe following are a few commonly used attributes\n\n| Attribute | Description | Notes |\n|---|---|---|\n| `provider` | AI provider name | Required |\n| `credential_name` | Name of the `DBMS_CLOUD` credential | Required |\n| `object_list` | JSON array of schemas/tables/views allowed for NL2SQL | Optional in 26ai; required in 19c |\n| `object_list_mode` | Specifies whether to send metadata for all objects in object_list or the most relevant objects to the LLM | Optional; values `all` or `automated`; 26ai feature |\n| `model` | Provider model name | Optional; exact values vary by provider |\n| `max_tokens` | Maximum response tokens | Optional; default is provider/package dependent |\n| `temperature` | Randomness for generation | Optional; Lower values are more deterministic |\n| `seed` | Enhance reproducible results or results with less variability from the LLM | Optional |\n| `comments` | Include table/column comments in prompt metadata | Optional; true/false |\n| `constraints` | Enable referential integrity constraints in metadata sent to LLM | Optional; true/false |\n| `annotations` | Enable referential integrity constraints in metadata sent to LLM | Optional; true/false; 26 AI feature |\n| `conversation` | Enable short-term conversation history | Optional boolean |\n\nSee [Select AI Profile Attributes](https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/dbms-cloud-ai-package.html#GUID-12D91681-B51C-48E0-93FD-9ABC67B0F375) for more information.\n\n```sql\n-- Profile with comments enabled and more deterministic generation\nBEGIN\n DBMS_CLOUD_AI.CREATE_PROFILE(\n profile_name => 'PRECISE_PROFILE',\n attributes => '{\"provider\": \"openai\",\n \"credential_name\": \"OPENAI_CRED\",\n \"object_list\": [{\"owner\": \"SALES\"}],\n \"comments\": true,\n \"seed\": 12345,\n \"temperature\": 0}'\n );\nEND;\n/\n```\n\n## Improving Results with Table Comments\n\nOracle can send table and column comments to the LLM when `comments` is enabled in the profile. Well-commented schemas generally produce better SQL. On 26ai, annotations are also supported using the `annotations` attribute.\n\n```sql\n-- Add descriptive comments so the LLM understands the schema\nCOMMENT ON TABLE sales.orders IS\n 'Customer purchase orders. Each row is one order.';\n\nCOMMENT ON COLUMN sales.orders.status IS\n 'Order lifecycle status: PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED';\n\nCOMMENT ON COLUMN sales.orders.total_amount IS\n 'Order total in USD including tax and shipping';\n```\n\n## Managing Profiles\n\n```sql\n-- List profiles in your schema\nSELECT profile_name, status, description\nFROM user_cloud_ai_profiles\nORDER BY profile_name;\n\n-- List attributes for one profile\nSELECT profile_name, attribute_name, attribute_value\nFROM user_cloud_ai_profile_attributes\nWHERE profile_name = 'MY_AI_PROFILE'\nORDER BY attribute_name;\n\n-- Disable a profile\nEXEC DBMS_CLOUD_AI.DISABLE_PROFILE('MY_AI_PROFILE');\n\n-- Enable a profile\nEXEC DBMS_CLOUD_AI.ENABLE_PROFILE('MY_AI_PROFILE');\n\n-- Clear the active session profile without dropping it\nBEGIN\n DBMS_CLOUD_AI.CLEAR_PROFILE;\nEND;\n/\n\n-- Check the active session profile\nSELECT DBMS_CLOUD_AI.GET_PROFILE()\nFROM DUAL;\n\n-- Drop a profile\nBEGIN\n DBMS_CLOUD_AI.DROP_PROFILE(profile_name => 'MY_AI_PROFILE');\nEND;\n/\n```\n\n## Security Considerations\n\n- For `runsql`, `showsql`, and `explainsql`, Oracle sends schema metadata, not table contents, to the LLM.\n- Metadata can include object names, column names, data types, and optionally comments and other prompt-enrichment metadata.\n- `narrate` for both SQL and RAG, as well as synthetic data generation, can send result data or retrieved document content to the LLM.\n- An administrator can disable those data-sending features globally with `DBMS_CLOUD_AI.DISABLE_DATA_ACCESS`.\n- Generated SQL still runs with the current session user's privileges; VPD and row-level security apply normally.\n- `SELECT AI` cannot execute PL/SQL, DDL, or DML.\n\n```sql\n-- Administrator-only: disable sending result data and RAG content to the LLM\nBEGIN\n DBMS_CLOUD_AI.DISABLE_DATA_ACCESS();\nEND;\n/\n```\n\n## Using SELECT AI Programmatically\n\nUse `DBMS_CLOUD_AI.GENERATE` when you want stateless calls, per-call profile overrides, or programmatic use from PL/SQL or an application.\n\n```sql\nDECLARE\n v_result CLOB;\nBEGIN\n v_result := DBMS_CLOUD_AI.GENERATE(\n prompt => 'how many employees are in each department',\n profile_name => 'MY_AI_PROFILE',\n action => 'showsql'\n );\n\n DBMS_OUTPUT.PUT_LINE(v_result);\nEND;\n/\n```\n\n## Python Access\n\nFor Python applications, choose the interface based on how much Select AI-specific workflow you need:\n\n- Use `python-oracledb` to execute `SELECT AI ...` or call `DBMS_CLOUD_AI` / `DBMS_CLOUD_AI_AGENT` when your application already manages normal SQL execution.\n- Use Oracle's `select_ai` Python library when you need Python-native objects for profiles, conversations, vector indexes, synthetic data generation, feedback, async workflows, or agent teams.\n- Keep generic driver concerns such as pooling, binds, LOBs, and transaction control in `db/appdev/python-oracledb.md`.\n\n## Ambiguous Table Name Handling\n\nIf similar table names exist across schemas, narrow the profile scope with `object_list`.\n\nUse `SET_ATTRIBUTE` for a single attribute or `SET_ATTRIBUTES` for multiple attributes. Do not use `SET_PROFILE` to edit attributes.\n\n```sql\n-- Narrow object_list to explicit owner.object combinations\nBEGIN\n DBMS_CLOUD_AI.SET_ATTRIBUTE(\n profile_name => 'MYAPP_AI',\n attribute_name => 'object_list',\n attribute_value => '[{\"owner\":\"HR\",\"name\":\"EMPLOYEES\"},\n {\"owner\":\"HR\",\"name\":\"DEPARTMENTS\"},\n {\"owner\":\"SALES\",\"name\":\"ORDERS\"}]'\n );\nEND;\n/\n```\n\n## Session vs Stateless Usage\n\n`SELECT AI` always uses the active session profile. For per-call overrides, use `DBMS_CLOUD_AI.GENERATE(profile_name => ...)`.\n\n```sql\n-- Set a default profile for the session\nEXEC DBMS_CLOUD_AI.SET_PROFILE('MYAPP_AI');\n\n-- Session-based SELECT AI call\nSELECT AI SHOWSQL list the top 10 customers by revenue;\n\n-- Stateless call with an explicit profile override\nSELECT DBMS_CLOUD_AI.GENERATE(\n prompt => 'list the top 10 customers by revenue',\n profile_name => 'FINANCE_AI',\n action => 'showsql'\n )\nFROM dual;\n\n-- Check the active session profile\nSELECT DBMS_CLOUD_AI.GET_PROFILE()\nFROM dual;\n```\n\n## Feedback for NL2SQL Refinement\n\nUse Select AI feedback to refine SQL generation for NL2SQL actions such as `RUNSQL`, `SHOWSQL`, and `EXPLAINSQL`. Feedback is not the correction mechanism for RAG grounding.\n\n```sql\n-- Feedback on a specific prompt\nSELECT AI FEEDBACK FOR QUERY\n \"select ai showsql how many watch histories in total\",\n please use sum instead of count;\n\n-- Feedback on a specific generated SQL statement\nSELECT AI FEEDBACK please use sum instead of count for sql_id 1v1z68ra6r9zf;\n\n-- Positive feedback on the most recent generated SQL\nSELECT AI FEEDBACK the result is correct;\n```\n\nUse `DBMS_CLOUD_AI.FEEDBACK` when feedback needs to come from SQL or PL/SQL rather than the `AI` keyword surface. Treat feedback as profile-affecting state: keep separate profiles for separate business domains, and avoid shared-session application patterns where unrelated users write feedback into the same profile.\n\n## Synthetic Data Generation\n\nSynthetic data generation uses `DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA`, not `CHAT`, `NARRATE`, or ordinary SQL generation. Use it for development, testing, demos, or metadata clones where shape and variety matter more than production truth.\n\n```sql\n-- Single table form\nBEGIN\n DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA(\n profile_name => 'GENAI',\n owner_name => 'HR',\n object_name => 'EMPLOYEES',\n record_count => 100,\n user_prompt => 'Use realistic department and job combinations'\n );\nEND;\n/\n\n-- Multi-table form\nBEGIN\n DBMS_CLOUD_AI.GENERATE_SYNTHETIC_DATA(\n profile_name => 'GENAI',\n object_list => '[{\"owner\":\"HR\",\"name\":\"DEPARTMENTS\",\"record_count\":5},\n {\"owner\":\"HR\",\"name\":\"EMPLOYEES\",\"record_count\":100}]'\n );\nEND;\n/\n```\n\nLarge generation jobs are tracked in status tables named like `SYNTHETIC_DATA$\u003coperation_id>_STATUS`. Start with small `record_count` values, inspect the generated data, then scale.\n\n## Select AI Agent Lifecycle\n\nUse Select AI Agent when the workflow needs explicit teams, agents, tasks, tools, and multi-step orchestration. Simpler one-shot generation should stay with `SELECT AI \u003caction>` or `DBMS_CLOUD_AI.GENERATE`.\n\nThe normal lifecycle is:\n\n1. Create agents with `DBMS_CLOUD_AI_AGENT.CREATE_AGENT`.\n2. Create tools with `DBMS_CLOUD_AI_AGENT.CREATE_TOOL`.\n3. Create tasks with `DBMS_CLOUD_AI_AGENT.CREATE_TASK`.\n4. Assemble a team with `DBMS_CLOUD_AI_AGENT.CREATE_TEAM`.\n5. Run with `SELECT AI AGENT ...` or `DBMS_CLOUD_AI_AGENT.RUN_TEAM`.\n\nBuilt-in tool categories include SQL, RAG, web search, notification, and custom PL/SQL. Treat tool creation as explicit configuration; do not assume that a prompt can use arbitrary tools unless those tools were created and enabled.\n\n## History and Observability\n\nUse `V$CLOUD_AI_SQL` for SQL-generation history and the conversation views for conversation-backed prompt history.\n\n```sql\n-- Find the SQL_ID for a previously issued Select AI statement\nSELECT sql_id\nFROM v$cloud_ai_sql\nWHERE sql_text = 'select ai showsql how many movies are in each genre';\n\n-- Review conversation prompt history in your schema\nSELECT conversation_id,\n prompt_action,\n prompt,\n prompt_response,\n created\nFROM user_cloud_ai_conversation_prompts\nORDER BY created DESC\nFETCH FIRST 20 ROWS ONLY;\n```\n\n## Error and Provider Outage Handling\n\nProvider or network failures surface through `DBMS_CLOUD_AI.GENERATE`, commonly as `ORA-20000` or `ORA-29273`. Handle those errors in PL/SQL and fall back to manual review when needed.\n\n```sql\nDECLARE\n v_sql CLOB;\nBEGIN\n v_sql := DBMS_CLOUD_AI.GENERATE(\n prompt => 'how many employees are in each department',\n profile_name => 'MY_AI_PROFILE',\n action => 'showsql'\n );\nEXCEPTION\n WHEN OTHERS THEN\n IF SQLCODE IN (-20000, -29273) THEN\n v_sql := NULL;\n DBMS_OUTPUT.PUT_LINE('Provider call failed; fall back to manual SQL review.');\n ELSE\n RAISE;\n END IF;\nEND;\n/\n```\n\n## Views vs Base Tables in object_list\n\n- Select AI works with both views and base tables in `object_list`\n- Prefer views in production so you can hide sensitive columns, stabilize naming, and encode common joins or derived metrics\n- Views are especially useful when the base schema uses cryptic column names or exposes columns the LLM should never reference\n\n```sql\n-- Create a SELECT AI-safe view with only the columns you want exposed\nCREATE OR REPLACE VIEW ai_employees AS\nSELECT employee_id, job_id, department_id, hire_date, salary\nFROM employees;\n\n-- Include the view in the AI profile instead of the base table\n-- \"object_list\": [{\"owner\":\"HR\",\"name\":\"AI_EMPLOYEES\"}]\n```\n\n## Best Practices\n\n- Use `SHOWSQL` first and review the generated SQL before using `RUNSQL` in production workflows.\n- Keep the metadata scope small by using a focused `object_list` or schema-level scoping.\n- Prefer views with business-friendly names, pre-joined relationships, and precomputed KPIs for common prompts.\n- Enable `comments` and/or `annotations` and add meaningful table and column content.\n- Use low `temperature` values, typically `0`, for more deterministic SQL generation.\n- In Database Actions or APEX Service, use `DBMS_CLOUD_AI.GENERATE` instead of `SELECT AI`.\n- Treat `narrate` as a prose output path, not a structured data API.\n- For translation, use an OCI profile and configure `target_language`.\n\n## Common Mistakes\n\n**Trying to edit attributes with `SET_PROFILE`** — use `SET_ATTRIBUTE` or `SET_ATTRIBUTES` to change profile metadata such as `object_list`, `temperature`, or `comments`.\n\n**Using a single profile for the whole enterprise schema** — too much metadata increases ambiguity and token pressure. Specify minimal set of objects or use `automated` object_list_mode.\n\n**Exposing raw base tables instead of views** — this makes it easier for the LLM to choose the wrong columns or expose sensitive fields.\n\n**Using `narrate` when you need structured output** — `narrate` returns prose; use `showsql` or `runsql` when you need SQL or rows.\n\n**Assuming the command line `SELECT AI` works everywhere** — use `DBMS_CLOUD_AI.GENERATE` in Database Actions and APEX Service.\n\n## Oracle Version Notes\n\n- **26ai**: This guide targets the 26ai Select AI feature set, including `showprompt`, feedback, summarize, translate, agent integration, richer observability, and broader provider support\n- **Autonomous Database 19c**: Supports Select AI summarization, but not the full 26ai feature set described in this guide\n\n## See Also\n\n- [AI Profiles and Provider Configuration](../features/ai-profiles.md) — Provider-specific profile setup details\n- [DBMS_VECTOR and DBMS_VECTOR_CHAIN](../features/dbms-vector.md) — RAG and vector-enabled AI workflows\n- [AI Vector Search in Oracle](../features/vector-search.md) — Embeddings and semantic retrieval in Oracle\n- [Natural Language to SQL Mapping Patterns](../agent/nl-to-sql-patterns.md) — Manual NL-to-SQL guidance for agent workflows\n\n## Sources\n\n- [Select AI Online Documentation](https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/select-ai.html)\n- [Select AI Agent Online Documentation](https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/select-ai-agent1.html)\n- [Oracle AI Database 26ai Select AI User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/oracle-database-select-ai-users-guide.pdf)\n- [DBMS_CLOUD_AI Package Reference](https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-package.html)\n- [DBMS_CLOUD_AI_AGENT Package Reference](https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/dbms-cloud-ai-agent-package.html)\n- [DBMS_CLOUD_AI Views](https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/dbms_cloud_ai-views.html)\n- [Examples of Using Select AI](https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/select-ai-examples.html)\n- [Select AI for Python](https://docs.oracle.com/en/database/oracle/oracle-database/26/selai/select-ai-python.html)\n- [Synthetic Data Generation](https://docs.oracle.com/en-us/iaas/autonomous-database-shared/doc/select-ai-synthetic-data-generation.html)\n- [Verify, Observe, and Secure your Generative AI usage with Oracle Autonomous AI Database Select AI](https://blogs.oracle.com/machinelearning/verify-observe-and-secure-your-gen-ai-usage-with-adb-select-ai)\n- [6 Simple Tips for Better Text-to-SQL Generation using Oracle Autonomous Database Select AI](https://blogs.oracle.com/machinelearning/6-simple-tips-for-better-texttosql-generation-using-oracle-autonomous-database-select-ai)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":21022,"content_sha256":"19611d0764d60a0227b4a6363b96dc81906394981f2d075adc5a366e769e2817"},{"filename":"features/vector-search.md","content":"# AI Vector Search in Oracle\n\nOracle 26ai includes native AI Vector Search, allowing vector embeddings to be stored, indexed, and queried directly alongside relational data. This enables similarity search, retrieval-augmented generation (RAG), and hybrid AI+SQL workloads without an external vector database.\n\nVector support was introduced in Oracle 23ai and is fully available in Oracle 26ai.\n\n## The VECTOR Data Type\n\n```sql\n-- Minimal runnable example with a 3-dimensional vector column\nCREATE TABLE documents (\n id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n content VARCHAR2(4000),\n source VARCHAR2(255),\n embedding VECTOR(3, FLOAT32)\n);\n\n-- Insert a vector\nINSERT INTO documents (content, source, embedding)\nVALUES (\n 'Oracle Database supports native vector search.',\n 'docs',\n TO_VECTOR('[0.023, -0.047, 0.112]')\n);\n\n-- Production embedding columns typically use dimensions such as 768, 1024, 1536, or 3072\n```\n\n### Supported Dimension Sizes and Storage Formats\n\n| Format | Description | Use Case |\n|---|---|---|\n| `FLOAT32` | 32-bit floating point | Default; good balance of precision and size |\n| `FLOAT64` | 64-bit double precision | Maximum precision |\n| `INT8` | 8-bit integer (quantized) | Compact storage, faster search, slight precision loss |\n| `*` (flexible) | Accepts any format | Use when format varies by source |\n\n```sql\n-- Flexible dimension/format column (accepts any embedding)\nembedding VECTOR(*, *)\n\n-- Fixed 768 dimensions, 8-bit quantized (e.g., compact models)\nembedding VECTOR(768, INT8)\n```\n\n## Vector Distance Functions\n\nOracle provides `VECTOR_DISTANCE()` and shorthand operators for similarity search.\n\n```sql\n-- Find the 5 most similar documents to a query vector\nSELECT id, content,\n VECTOR_DISTANCE(embedding, :query_vector, COSINE) AS distance\nFROM documents\nORDER BY distance\nFETCH FIRST 5 ROWS ONLY;\n```\n\n### Distance Metrics\n\n| Metric | Function Constant | Best For |\n|---|---|---|\n| Cosine similarity | `COSINE` | Text embeddings (most common) |\n| Euclidean (L2) | `EUCLIDEAN` or `L2` | Image embeddings, spatial data |\n| Dot product | `DOT` | When vectors are pre-normalized |\n| Manhattan (L1) | `L1_DISTANCE` | Sparse vectors |\n| Hamming | `HAMMING` | Binary/bit vectors |\n\n```sql\n-- Shorthand operators (introduced in Oracle 23ai)\n-- \u003c=> : cosine distance\n-- \u003c-> : euclidean distance\n-- \u003c#> : negative dot product\n\nSELECT id, content\nFROM documents\nORDER BY embedding \u003c=> :query_vector\nFETCH FIRST 10 ROWS ONLY;\n```\n\n## Vector Indexes\n\nWithout an index, Oracle performs an exact (brute-force) search. For large datasets, create a vector index for approximate nearest neighbor (ANN) search.\n\n### HNSW Index (Hierarchical Navigable Small World)\n\nBest for static or slowly changing datasets. Offers the best query performance.\n\n```sql\nCREATE VECTOR INDEX docs_hnsw_idx\nON documents (embedding)\nORGANIZATION INMEMORY NEIGHBOR GRAPH\nDISTANCE COSINE\nWITH TARGET ACCURACY 95;\n```\n\n### IVF Index (Inverted File)\n\nBetter for datasets with frequent inserts. Uses clustering to partition the vector space.\n\n```sql\nCREATE VECTOR INDEX docs_ivf_idx\nON documents (embedding)\nORGANIZATION NEIGHBOR PARTITIONS\nDISTANCE COSINE\nWITH TARGET ACCURACY 90\nPARAMETERS (TYPE IVF, NEIGHBOR PARTITIONS 64);\n```\n\n### Index Accuracy vs. Performance Trade-off\n\n```sql\n-- Higher accuracy = slower build, slower query, but fewer missed results\nWITH TARGET ACCURACY 99 -- near-exact, slower\nWITH TARGET ACCURACY 80 -- faster, some recall loss\n```\n\n## Combining Vector Search with Relational Filters\n\nOne of Oracle's key advantages: vector search and SQL predicates in the same query.\n\n```sql\n-- Semantic search filtered by relational conditions\nSELECT d.id,\n d.content,\n d.source,\n VECTOR_DISTANCE(d.embedding, :query_vec, COSINE) AS score\nFROM documents d\nWHERE d.source = 'docs' -- relational filter\n AND d.created_date >= SYSDATE - 30 -- date filter\nORDER BY score\nFETCH FIRST 5 ROWS ONLY;\n```\n\n```sql\n-- Join vector results with relational data\nSELECT p.product_name,\n p.price,\n VECTOR_DISTANCE(p.description_vec, :query_vec, COSINE) AS relevance\nFROM products p\nJOIN categories c ON p.category_id = c.id\nWHERE c.name = 'Electronics'\n AND p.in_stock = 'Y'\nORDER BY relevance\nFETCH FIRST 10 ROWS ONLY;\n```\n\n## RAG Pattern: Retrieval-Augmented Generation\n\nThe typical RAG pipeline with Oracle:\n\n```sql\n-- Step 1: Store chunked document embeddings\nINSERT INTO doc_chunks (doc_id, chunk_seq, chunk_text, embedding)\nSELECT doc_id,\n chunk_seq,\n chunk_text,\n TO_VECTOR(embedding_json)\nFROM staging_chunks;\n\n-- Step 2: At query time, find relevant chunks\nSELECT chunk_text\nFROM doc_chunks\nORDER BY embedding \u003c=> :user_query_embedding\nFETCH FIRST 5 ROWS ONLY;\n\n-- Step 3: Pass retrieved chunks + user question to LLM (via SELECT AI or app layer)\n```\n\n## Checking Vector Index Usage\n\n```sql\n-- Confirm index is being used in execution plan\nEXPLAIN PLAN FOR\nSELECT id FROM documents\nORDER BY embedding \u003c=> :q FETCH FIRST 5 ROWS ONLY;\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Look for: VECTOR INDEX HNSW SCAN or VECTOR INDEX IVF SCAN\n\n-- Monitor vector index memory\nSELECT index_name,\n num_vectors,\n ROUND(allocated_bytes / 1024 / 1024 / 1024, 2) AS allocated_gb,\n ROUND(used_bytes / 1024 / 1024 / 1024, 2) AS used_gb,\n default_accuracy\nFROM v$vector_index;\n```\n\n## Multi-Vector Queries\n\nQuerying across multiple VECTOR columns or combining multiple similarity searches.\n\n```sql\n-- Find documents similar to BOTH a text query AND an image query\n-- (cross-modal search: text + image embeddings in same row)\nSELECT doc_id, title,\n text_embedding \u003c=> :query_text_vec AS text_dist,\n image_embedding \u003c=> :query_image_vec AS image_dist\nFROM documents\nORDER BY (text_embedding \u003c=> :query_text_vec) * 0.6 +\n (image_embedding \u003c=> :query_image_vec) * 0.4 -- weighted combination\nFETCH FIRST 10 ROWS ONLY;\n\n-- Reciprocal Rank Fusion (RRF) — combine rankings from two searches\nWITH text_search AS (\n SELECT doc_id,\n ROW_NUMBER() OVER (ORDER BY embedding \u003c=> :query_vec) AS rn\n FROM documents\n FETCH FIRST 100 ROWS ONLY\n),\nkeyword_search AS (\n SELECT doc_id,\n ROW_NUMBER() OVER (ORDER BY score(1) DESC) AS rn\n FROM documents\n WHERE CONTAINS(content, :keywords, 1) > 0\n FETCH FIRST 100 ROWS ONLY\n)\nSELECT COALESCE(t.doc_id, k.doc_id) AS doc_id,\n 1/(60 + NVL(t.rn, 100)) + 1/(60 + NVL(k.rn, 100)) AS rrf_score\nFROM text_search t\nFULL JOIN keyword_search k ON t.doc_id = k.doc_id\nORDER BY rrf_score DESC\nFETCH FIRST 10 ROWS ONLY;\n```\n\n## Hybrid Vector Search\n\nUse hybrid vector search when retrieval needs both semantic similarity and keyword or structured filtering. Oracle supports this as an application query pattern and as a first-class hybrid index path.\n\n```sql\nCREATE HYBRID VECTOR INDEX docs_hybrid_idx ON doc_chunks(chunk_text)\nPARAMETERS('MODEL my_embedding_model');\n```\n\nQuery hybrid indexes with `DBMS_HYBRID_VECTOR.SEARCH` when you need one call that combines vector and text criteria.\n\n```sql\nSELECT DBMS_HYBRID_VECTOR.SEARCH(\n json('{ \"hybrid_index_name\" : \"docs_hybrid_idx\",\n \"vector\" : { \"search_text\" : \"invoice payment dispute\" },\n \"text\" : { \"contains\" : \"customer AND overdue\" },\n \"return\" : { \"topN\" : 10 }\n }'))\nFROM dual;\n```\n\nUse `text.contains` or `text.search_text` for lexical constraints. Use `filter_by` for business predicates such as status, region, or price. Use vector `filter_type` only when tuning how filtering interacts with vector index evaluation; it is not the same thing as a business filter.\n\n## Bulk Load: Disable Index, Load, Rebuild\n\nWhen loading millions of vectors, disable the vector index first, load, then rebuild.\n\n```sql\n-- 1. Drop the vector index before bulk load\nDROP INDEX doc_chunks_hnsw_idx;\n\n-- 2. Bulk load vectors\nINSERT INTO doc_chunks (doc_id, chunk_seq, chunk_text, embedding)\nSELECT doc_id, chunk_seq, chunk_text, embedding FROM staging_chunks;\nCOMMIT;\n\n-- 3. Rebuild the HNSW index after load (much faster than incremental updates)\nCREATE VECTOR INDEX doc_chunks_hnsw_idx ON doc_chunks(embedding)\nORGANIZATION INMEMORY NEIGHBOR GRAPH\nDISTANCE COSINE\nWITH TARGET ACCURACY 95;\n\n-- Why: HNSW indexes are built in-memory; inserting into an existing HNSW index\n-- one row at a time during bulk load is ~10x slower than post-load rebuild\n```\n\n## HNSW Index Memory Sizing\n\n```sql\n-- Check current vector index memory allocation\nSHOW PARAMETER vector_memory_size;\n\n-- Estimate required memory for HNSW index\n-- Rule of thumb: dimensions × 4 bytes × num_vectors × 1.3 (overhead factor)\n-- Example: 1536 dims × 4B × 1M vectors × 1.3 ≈ ~8 GB\nSELECT index_name,\n num_vectors,\n embedding_dimension_count,\n ROUND(allocated_bytes / 1024 / 1024 / 1024, 2) AS allocated_gb,\n ROUND(used_bytes / 1024 / 1024 / 1024, 2) AS used_gb\nFROM v$vector_index\nORDER BY index_name;\n\n-- If VECTOR_MEMORY_SIZE is too small, HNSW falls back to disk (much slower)\n-- Increase (requires restart or ALTER SYSTEM):\nALTER SYSTEM SET vector_memory_size = 8G SCOPE = SPFILE;\n```\n\n## V$VECTOR_INDEX Monitoring\n\n```sql\n-- Monitor HNSW index memory and accuracy\nSELECT index_name,\n num_vectors,\n default_accuracy,\n accuracy_num_neighbors,\n ROUND(allocated_bytes / 1024 / 1024 / 1024, 2) AS allocated_gb,\n ROUND(used_bytes / 1024 / 1024 / 1024, 2) AS used_gb\nFROM v$vector_index\nORDER BY index_name;\n\n-- Check if index is fully loaded into memory\nSELECT index_name,\n CASE WHEN used_bytes > 0 THEN 'ALLOCATED' ELSE 'NOT ALLOCATED' END AS storage_mode\nFROM v$vector_index;\n```\n\n## Parallel Vector Index Creation\n\n```sql\n-- Create HNSW index with parallelism (faster for large datasets)\nCREATE VECTOR INDEX doc_chunks_hnsw_idx ON doc_chunks(embedding)\nORGANIZATION INMEMORY NEIGHBOR GRAPH\nDISTANCE COSINE\nWITH TARGET ACCURACY 95\nPARALLEL 4; -- use 4 parallel workers\n\n-- After creation, index maintenance is single-threaded (DML inserts are serial)\n-- Check index creation progress in V$SESSION_LONGOPS\nSELECT sid, serial#, opname, target, sofar, totalwork,\n ROUND(sofar/totalwork*100, 1) AS pct_done\nFROM v$session_longops\nWHERE opname LIKE '%VECTOR INDEX%'\n AND totalwork > 0;\n```\n\n## Best Practices\n\n- Use `FLOAT32` unless you have a specific reason for `FLOAT64` — it halves storage with negligible quality loss for most models\n- Match the distance metric to your embedding model's training objective (most text models use cosine)\n- Set `TARGET ACCURACY 95` as a starting point; tune down if query speed is insufficient\n- Always combine vector search with relational predicates to reduce the search space\n- For RAG: chunk documents at ~500 tokens with 10–20% overlap for best retrieval quality\n- Rebuild or reindex HNSW periodically if the dataset changes significantly (IVF handles inserts better)\n- Store original text alongside the embedding — you need it for LLM context\n\n## Common Mistakes\n\n**Wrong distance metric for the model** — using `EUCLIDEAN` with a cosine-trained embedding model degrades results significantly. Check your model documentation.\n\n**Embedding dimension mismatch** — `text-embedding-3-small` outputs 1536 dims; `text-embedding-3-large` can output up to 3072. Declare the exact dimension in the column type.\n\n**No relational pre-filter** — scanning 10M vectors for a single user's 1000 documents wastes resources. Always add `WHERE user_id = :uid` before the vector sort.\n\n**Skipping the vector index** — for tables > 100K rows, an unindexed vector search is a full table scan of all vectors. Always create a HNSW or IVF index for production.\n\n**Storing embeddings as VARCHAR2/CLOB** — pre-23ai workarounds stored vectors as JSON arrays. Migrate to the native `VECTOR` type for proper indexing and distance functions.\n\n## Oracle Version Notes (19c vs 26ai)\n\n- **19c**: No native VECTOR type; workarounds used VARCHAR2/CLOB with external similarity calculation\n- **23ai**: VECTOR data type introduced; HNSW and IVF indexes; `VECTOR_DISTANCE()` and shorthand operators\n- **26ai**: Full production support; enhanced index performance; `DBMS_VECTOR`, `DBMS_VECTOR_CHAIN`, and `DBMS_HYBRID_VECTOR` for pipeline automation and hybrid retrieval; `SELECT AI` integration\n\n## See Also\n\n- [SELECT AI in Oracle 26ai](../features/select-ai.md) — Natural language to SQL using vector-powered context\n- [DBMS_VECTOR and DBMS_VECTOR_CHAIN](../features/dbms-vector.md) — Automated embedding generation and RAG pipelines\n- [AI Profiles and Provider Configuration](../features/ai-profiles.md) — Connecting Oracle to OpenAI, Cohere, OCI GenAI\n\n## Sources\n\n- [Oracle AI Vector Search User's Guide](https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/)\n- [VECTOR Data Type — Oracle SQL Language Reference](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/data-types.html)\n- [VECTOR_DISTANCE Function](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/VECTOR_DISTANCE.html)\n- [CREATE HYBRID VECTOR INDEX](https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-hybrid-vector-index.html)\n- [DBMS_HYBRID_VECTOR](https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/dbms_hybrid_vector-vecse.html)\n- [Oracle AI Vector Search Blog](https://blogs.oracle.com/database/post/oracle-ai-vector-search)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":13541,"content_sha256":"d28dcdad24b94969427289f71bbe619aa031c0ce4957f3b1d33976ce7a642eb8"},{"filename":"features/virtual-columns.md","content":"# Oracle Virtual Columns\n\n## Overview\n\nA **virtual column** is a column whose value is not physically stored on disk. Instead, it is defined as a deterministic expression that Oracle evaluates on the fly whenever the column is referenced — either in a query, an index, a constraint, or a partition key.\n\nVirtual columns were introduced in Oracle 11g Release 1 and provide a way to expose derived data as first-class column citizens without the storage overhead, the maintenance burden of triggers, or the query complexity of inline expressions.\n\n**When virtual columns are useful:**\n- Exposing a frequently used computed expression as a named column\n- Creating an index on a complex expression without writing a function-based index\n- Using a computed value as a partitioning key\n- Enforcing business rules via check constraints on derived values\n- Providing stable interfaces for views and applications when underlying logic changes\n\n---\n\n## Defining Virtual Columns\n\n### Basic Syntax\n\n```sql\ncolumn_name [data_type] [GENERATED ALWAYS] AS (expression) [VIRTUAL]\n```\n\n- `GENERATED ALWAYS AS (expression)` is mandatory syntax.\n- The `VIRTUAL` keyword is optional but recommended for clarity.\n- The data type is optional; Oracle infers it from the expression. Explicit types must be compatible with the expression result.\n\n### Simple Virtual Column\n\n```sql\nCREATE TABLE employees (\n employee_id NUMBER(6) NOT NULL,\n first_name VARCHAR2(50) NOT NULL,\n last_name VARCHAR2(50) NOT NULL,\n salary NUMBER(10,2) NOT NULL,\n commission_pct NUMBER(3,2),\n\n -- Fully-qualified name for display; no stored data\n full_name VARCHAR2(101) GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL,\n\n -- Annual salary including commission\n annual_comp NUMBER GENERATED ALWAYS AS (\n salary * 12 * NVL(1 + commission_pct, 1)\n ) VIRTUAL,\n\n CONSTRAINT pk_employees PRIMARY KEY (employee_id)\n);\n```\n\n### Adding a Virtual Column to an Existing Table\n\n```sql\nALTER TABLE employees\nADD (\n salary_band VARCHAR2(10) GENERATED ALWAYS AS (\n CASE\n WHEN salary \u003c 30000 THEN 'LOW'\n WHEN salary \u003c 80000 THEN 'MEDIUM'\n WHEN salary \u003c 150000 THEN 'HIGH'\n ELSE 'EXECUTIVE'\n END\n ) VIRTUAL\n);\n```\n\n### Querying Virtual Columns\n\nVirtual columns are indistinguishable from regular columns in queries:\n\n```sql\nSELECT employee_id, full_name, salary, annual_comp, salary_band\nFROM employees\nWHERE salary_band = 'HIGH'\nORDER BY annual_comp DESC;\n```\n\nOracle evaluates the expression inline — it does not store the result. The `annual_comp` predicate is evaluated per row during the scan.\n\n---\n\n## Function-Based Virtual Columns\n\nVirtual columns can call **deterministic** PL/SQL functions:\n\n```sql\nCREATE OR REPLACE FUNCTION fiscal_year(p_date IN DATE)\nRETURN NUMBER DETERMINISTIC AS\nBEGIN\n -- Fiscal year starts April 1\n RETURN CASE\n WHEN EXTRACT(MONTH FROM p_date) >= 4\n THEN EXTRACT(YEAR FROM p_date)\n ELSE EXTRACT(YEAR FROM p_date) - 1\n END;\nEND fiscal_year;\n/\n\nCREATE TABLE sales_orders (\n order_id NUMBER NOT NULL,\n order_date DATE NOT NULL,\n customer_id NUMBER NOT NULL,\n total_amount NUMBER(12,2) NOT NULL,\n\n -- Virtual column using a deterministic function\n fiscal_yr NUMBER GENERATED ALWAYS AS (fiscal_year(order_date)) VIRTUAL,\n\n -- Built-in function: truncate to month for time-series grouping\n order_month DATE GENERATED ALWAYS AS (TRUNC(order_date, 'MM')) VIRTUAL,\n\n CONSTRAINT pk_sales_orders PRIMARY KEY (order_id)\n);\n```\n\n**The function MUST be declared `DETERMINISTIC`.** If a non-deterministic function is used as a virtual column expression, Oracle will raise an error or produce unreliable results when the column is indexed.\n\n---\n\n## Indexing Virtual Columns\n\nOne of the most important use cases: create a B-tree index directly on a virtual column. This is equivalent to a function-based index but with better usability.\n\n```sql\n-- Index on a virtual column for salary band queries\nCREATE INDEX idx_emp_salary_band ON employees (salary_band);\n\n-- Composite index: fiscal year + customer for reporting queries\nCREATE INDEX idx_orders_fiscal_cust ON sales_orders (fiscal_yr, customer_id);\n\n-- Query that uses the virtual column index (optimizer can use the index)\nSELECT customer_id, COUNT(*), SUM(total_amount)\nFROM sales_orders\nWHERE fiscal_yr = 2025\nGROUP BY customer_id;\n```\n\n### Verifying Index Usage on Virtual Columns\n\n```sql\nEXPLAIN PLAN FOR\n SELECT employee_id, full_name\n FROM employees\n WHERE salary_band = 'EXECUTIVE';\n\nSELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);\n-- Should show: INDEX RANGE SCAN on IDX_EMP_SALARY_BAND\n```\n\n---\n\n## Virtual Columns as Partition Keys\n\nVirtual columns are particularly powerful as **partition keys**, enabling you to partition on a derived value without denormalizing data.\n\n```sql\n-- Partition a large transaction table by fiscal year\nCREATE TABLE financial_transactions (\n txn_id NUMBER NOT NULL,\n txn_date DATE NOT NULL,\n account_id NUMBER NOT NULL,\n amount NUMBER(15,2) NOT NULL,\n txn_type VARCHAR2(20),\n\n -- Virtual column used as the partition key\n txn_fiscal_yr NUMBER GENERATED ALWAYS AS (fiscal_year(txn_date)) VIRTUAL\n)\nPARTITION BY RANGE (txn_fiscal_yr) (\n PARTITION p_fy2022 VALUES LESS THAN (2023),\n PARTITION p_fy2023 VALUES LESS THAN (2024),\n PARTITION p_fy2024 VALUES LESS THAN (2025),\n PARTITION p_fy2025 VALUES LESS THAN (2026),\n PARTITION p_future VALUES LESS THAN (MAXVALUE)\n);\n```\n\nWith this design:\n- A query `WHERE txn_date BETWEEN DATE '2024-04-01' AND DATE '2025-03-31'` triggers partition pruning if the optimizer can resolve `fiscal_year(txn_date)` to a range.\n- More commonly, query on `txn_fiscal_yr = 2024` directly for reliable partition pruning.\n\n```sql\n-- Direct partition pruning via virtual column\nSELECT SUM(amount)\nFROM financial_transactions\nWHERE txn_fiscal_yr = 2024\n AND txn_type = 'DEBIT';\n```\n\n---\n\n## Virtual Columns with Check Constraints\n\n```sql\nCREATE TABLE orders (\n order_id NUMBER PRIMARY KEY,\n order_date DATE NOT NULL,\n ship_date DATE,\n order_amount NUMBER(12,2) NOT NULL,\n discount_pct NUMBER(4,2) DEFAULT 0,\n\n -- Virtual column\n net_amount NUMBER GENERATED ALWAYS AS (order_amount * (1 - discount_pct/100)) VIRTUAL,\n\n -- Check constraint on the virtual column\n CONSTRAINT chk_net_positive CHECK (net_amount > 0),\n CONSTRAINT chk_ship_after_order CHECK (ship_date IS NULL OR ship_date >= order_date)\n);\n```\n\n---\n\n## Viewing Virtual Column Metadata\n\n```sql\n-- List virtual columns in a table\nSELECT column_name,\n data_type,\n data_length,\n nullable,\n virtual_column,\n data_default -- stores the expression\nFROM user_tab_columns\nWHERE table_name = 'EMPLOYEES'\n AND virtual_column = 'YES';\n\n-- Expression details for virtual columns\nSELECT column_name, data_default\nFROM user_tab_cols\nWHERE table_name = 'EMPLOYEES'\n AND virtual_column = 'YES';\n\n-- Check if any indexes are on virtual columns\nSELECT ic.index_name, ic.column_name, tc.virtual_column\nFROM user_ind_columns ic\nJOIN user_tab_cols tc ON tc.table_name = ic.table_name\n AND tc.column_name = ic.column_name\nWHERE ic.table_name = 'EMPLOYEES'\n AND tc.virtual_column = 'YES';\n```\n\n---\n\n## Limitations and Gotchas\n\n### What Expressions Are Allowed\n\nVirtual column expressions **must** be:\n- Deterministic (same inputs always produce the same output)\n- Self-contained within the row (can only reference columns of the same row)\n- Using built-in SQL functions or deterministic PL/SQL functions\n- Not referencing other virtual columns in the same table (Oracle 11g/12c; relaxed in later releases — verify your version)\n- Not containing subqueries, aggregate functions, or `ROWNUM`/`ROWID`/`LEVEL`\n\n### Storage and DML Behavior\n\n```sql\n-- You CANNOT insert into or update a virtual column\n-- This will raise ORA-54013\nINSERT INTO employees (employee_id, first_name, last_name, salary, full_name)\nVALUES (1001, 'Jane', 'Smith', 75000, 'Jane Smith'); -- ERROR\n\n-- Correct: omit virtual columns from INSERT\nINSERT INTO employees (employee_id, first_name, last_name, salary)\nVALUES (1001, 'Jane', 'Smith', 75000);\n\n-- You CAN reference virtual columns in SELECT and WHERE\nSELECT * FROM employees WHERE full_name = 'Jane Smith';\n```\n\n### Statistics and Virtual Columns\n\n```sql\n-- DBMS_STATS can gather statistics on virtual columns,\n-- but the METHOD_OPT default ('FOR ALL COLUMNS') includes them.\n-- If the expression is complex, stat gathering may be slower.\n-- You can exclude virtual columns explicitly:\nBEGIN\n DBMS_STATS.GATHER_TABLE_STATS(\n ownname => 'APPSCHEMA',\n tabname => 'EMPLOYEES',\n method_opt => 'FOR ALL REAL COLUMNS SIZE AUTO', -- skip virtual columns\n cascade => TRUE\n );\nEND;\n/\n```\n\n### Export and Import Considerations\n\nVirtual column expressions are stored as metadata in the data dictionary. When using Data Pump (`expdp`/`impdp`), the expressions are exported in the DDL. However:\n- If the expression references a user-defined PL/SQL function, that function must exist in the target schema before importing the table.\n- If the function signature changes between export and import, the virtual column may be invalid on import.\n\n### Virtual Columns in External Tables\n\nVirtual columns are **not supported** on external tables or on object-relational tables (tables with `REF` columns in certain configurations). Attempting to add one raises `ORA-30553`.\n\n### Performance Consideration: Expression Evaluation Cost\n\nVirtual columns are re-evaluated every time they are referenced in a query — unless an index covers the column. For a table with millions of rows and a complex PL/SQL function as the expression, a full-table scan referencing the virtual column repeatedly evaluates the function. **Index the virtual column** for any predicate that will be used in a WHERE clause.\n\n---\n\n## Best Practices\n\n- **Declare expressions in `DETERMINISTIC` PL/SQL functions** rather than embedding complex logic inline in the column definition. This improves readability, makes expression changes easier (just recompile the function), and keeps the DDL clean.\n- **Index virtual columns used in WHERE clauses and JOIN conditions.** Without an index, Oracle evaluates the expression for every row during a full scan.\n- **Name virtual columns clearly** to distinguish them from stored columns. Some teams use a naming suffix like `_V` or `_CALC` (e.g., `ANNUAL_COMP_V`) to signal to developers that the column is not a physical store.\n- **Use virtual columns as partition keys** instead of adding redundant denormalized columns. This avoids the risk of the stored column becoming inconsistent with the base data.\n- **Test expression changes carefully.** When you alter the expression of a virtual column, any dependent indexes become stale and must be rebuilt. Oracle does not automatically invalidate or rebuild them.\n- **Document the business rule behind each virtual column** in the column's comment:\n\n```sql\nCOMMENT ON COLUMN employees.salary_band IS\n 'Derived salary classification: LOW (\u003c30K), MEDIUM (30K-80K), HIGH (80K-150K), EXECUTIVE (150K+). Virtual — not stored.';\n```\n\n---\n\n## Common Mistakes and How to Avoid Them\n\n**Mistake 1: Using a non-deterministic function**\nOracle may allow creation in some configurations but produce wrong results when the column is indexed, because the index may not match the runtime value. Always explicitly mark functions `DETERMINISTIC` and verify they truly are (e.g., they do not call `SYSDATE`, `DBMS_RANDOM`, or read from other tables).\n\n**Mistake 2: Expecting DML to populate virtual columns**\nDevelopers unfamiliar with virtual columns sometimes include them in INSERT or UPDATE statements. This raises `ORA-54013: INSERT operation disallowed on virtual columns`. Applications must be coded to omit virtual column names from DML.\n\n**Mistake 3: Altering the underlying function without rebuilding indexes**\nIf you change `fiscal_year()` to use a different fiscal calendar, the index `idx_orders_fiscal_cust` still contains values computed by the old function. You must `ALTER INDEX ... REBUILD` after any change to a function referenced by a virtual column index.\n\n**Mistake 4: Using virtual column expressions in ORDER BY without an index**\nSorting on an unindexed virtual column forces Oracle to evaluate the expression for every row before sorting. For large tables, this causes expensive full scans + sort operations. Always check execution plans.\n\n**Mistake 5: Referencing the virtual column in the same table's trigger**\n`BEFORE INSERT OR UPDATE` triggers fire before the virtual column value is accessible. If your trigger tries to read a virtual column, it may see NULL or stale data. Use the base column expressions directly in trigger logic instead.\n\n---\n\n\n## Oracle Version Notes (19c vs 26ai)\n\n- Baseline guidance in this file is valid for Oracle Database 19c unless a newer minimum version is explicitly called out.\n- Features marked as 21c, 23c, or 23ai should be treated as Oracle Database 26ai-capable features; keep 19c-compatible alternatives for mixed-version estates.\n- For dual-support environments, test syntax and package behavior in both 19c and 26ai because defaults and deprecations can differ by release update.\n\n## Sources\n\n- [Oracle Database SQL Language Reference: Virtual Columns 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html)\n- [Oracle Database Administrator's Guide: Managing Tables — Virtual Columns 19c](https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-tables.html)\n- [Oracle Database VLDB and Partitioning Guide: Partitioning by Virtual Column](https://docs.oracle.com/en/database/oracle/oracle-database/19/vldbg/partition-virtual-column.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14150,"content_sha256":"164d976646c348e5fae7601f233a46104464f5871454b39cf87b48c04c7dd7c6"},{"filename":"frameworks/dapper-oracle.md","content":"# Dapper + Oracle Database\n\n## Overview\n\nDapper is a lightweight .NET micro-ORM that extends `IDbConnection` with convenient query and execute methods. It works directly with ODP.NET's `OracleConnection`, giving you raw SQL control with automatic result mapping.\n\n```bash\ndotnet add package Dapper\ndotnet add package Oracle.ManagedDataAccess.Core\n```\n\n---\n\n## Connecting\n\n```csharp\nusing Oracle.ManagedDataAccess.Client;\nusing Dapper;\n\nstring connStr = \"User Id=hr;Password=password;Data Source=localhost:1521/freepdb1;\";\n\n// Create and open connection\nusing var conn = new OracleConnection(connStr);\nconn.Open();\n\n// Or use a factory / DI\n// builder.Services.AddScoped\u003cIDbConnection>(_ =>\n// new OracleConnection(connStr));\n```\n\n---\n\n## Querying\n\n### `Query\u003cT>` — Map Rows to Objects\n\n```csharp\npublic class Employee\n{\n public long EmployeeId { get; set; }\n public string LastName { get; set; }\n public decimal Salary { get; set; }\n public long DepartmentId { get; set; }\n}\n\nusing var conn = new OracleConnection(connStr);\n\n// Named bind parameters (: prefix for Oracle)\nvar employees = conn.Query\u003cEmployee>(\n @\"SELECT employee_id AS EmployeeId,\n last_name AS LastName,\n salary AS Salary,\n department_id AS DepartmentId\n FROM employees\n WHERE department_id = :deptId\n AND salary > :minSal\n ORDER BY last_name\",\n new { deptId = 60, minSal = 5000m }\n).ToList();\n```\n\n### `QuerySingle` / `QueryFirst`\n\n```csharp\nvar emp = conn.QuerySingleOrDefault\u003cEmployee>(\n \"SELECT employee_id AS EmployeeId, last_name AS LastName, salary AS Salary \" +\n \"FROM employees WHERE employee_id = :id\",\n new { id = 100 }\n);\n\nif (emp is null) Console.WriteLine(\"Not found\");\nelse Console.WriteLine($\"{emp.LastName}: {emp.Salary}\");\n```\n\n### Scalar Values\n\n```csharp\nvar count = conn.ExecuteScalar\u003cint>(\n \"SELECT COUNT(*) FROM employees WHERE department_id = :dept\",\n new { dept = 60 }\n);\n\nvar maxSal = conn.ExecuteScalar\u003cdecimal>(\n \"SELECT MAX(salary) FROM employees WHERE department_id = :dept\",\n new { dept = 60 }\n);\n```\n\n### Dynamic Results (no class needed)\n\n```csharp\nvar rows = conn.Query(\n \"SELECT last_name, salary FROM employees WHERE department_id = :dept\",\n new { dept = 60 }\n);\n\nforeach (var row in rows)\n{\n Console.WriteLine($\"{row.LAST_NAME}: {row.SALARY}\");\n}\n```\n\n---\n\n## DML\n\n### `Execute` — INSERT / UPDATE / DELETE\n\n```csharp\n// UPDATE\nint rowsAffected = conn.Execute(\n \"UPDATE employees SET salary = :sal WHERE employee_id = :id\",\n new { sal = 9500m, id = 100 }\n);\n\n// INSERT\nconn.Execute(\n @\"INSERT INTO employees (employee_id, last_name, email, salary, department_id)\n VALUES (employees_seq.NEXTVAL, :lastName, :email, :salary, :deptId)\",\n new { lastName = \"Smith\", email = \"[email protected]\", salary = 7500m, deptId = 60 }\n);\n```\n\n### Batch Insert\n\n```csharp\nvar newEmployees = new[]\n{\n new { lastName = \"Alice\", email = \"[email protected]\", salary = 6000m, deptId = 10 },\n new { lastName = \"Bob\", email = \"[email protected]\", salary = 7000m, deptId = 20 },\n};\n\nconn.Execute(\n @\"INSERT INTO employees (employee_id, last_name, email, salary, department_id)\n VALUES (employees_seq.NEXTVAL, :lastName, :email, :salary, :deptId)\",\n newEmployees // Dapper calls Execute once per row\n);\n```\n\n---\n\n## Transactions\n\n```csharp\nusing var conn = new OracleConnection(connStr);\nconn.Open();\nusing var txn = conn.BeginTransaction();\n\ntry\n{\n conn.Execute(\n \"UPDATE accounts SET balance = balance - :amt WHERE id = :from\",\n new { amt = 500m, from = 1 },\n transaction: txn\n );\n conn.Execute(\n \"UPDATE accounts SET balance = balance + :amt WHERE id = :to\",\n new { amt = 500m, to = 2 },\n transaction: txn\n );\n txn.Commit();\n}\ncatch\n{\n txn.Rollback();\n throw;\n}\n```\n\n---\n\n## PL/SQL Calls\n\n### Stored Procedure via `Execute`\n\n```csharp\nconn.Execute(\n \"BEGIN hr.update_salary(:id, :sal); END;\",\n new { id = 100, sal = 9500m }\n);\n```\n\n### OUT Parameters with `DynamicParameters`\n\n```csharp\nusing Oracle.ManagedDataAccess.Client;\n\nvar p = new DynamicParameters();\np.Add(\"p_dept_id\", 60, direction: ParameterDirection.Input);\np.Add(\"p_count\", dbType: DbType.Int32, direction: ParameterDirection.Output);\np.Add(\"p_avg_sal\", dbType: DbType.Decimal, direction: ParameterDirection.Output);\n\nconn.Execute(\"BEGIN hr.get_dept_stats(:p_dept_id, :p_count, :p_avg_sal); END;\", p);\n\nint count = p.Get\u003cint>(\"p_count\");\ndecimal avgSal = p.Get\u003cdecimal>(\"p_avg_sal\");\nConsole.WriteLine($\"Count: {count}, Avg Salary: {avgSal}\");\n```\n\n### Function Return Value\n\n```csharp\nvar p = new DynamicParameters();\np.Add(\"result\", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);\np.Add(\"p_dept_id\", 60, direction: ParameterDirection.Input);\n\nconn.Execute(\"BEGIN :result := hr.get_employee_count(:p_dept_id); END;\", p);\nint count = p.Get\u003cint>(\"result\");\n```\n\n---\n\n## Multi-Mapping (Joins)\n\n```csharp\npublic class Department\n{\n public long DepartmentId { get; set; }\n public string DepartmentName { get; set; }\n}\n\nvar sql = @\"\n SELECT e.employee_id AS EmployeeId,\n e.last_name AS LastName,\n e.salary AS Salary,\n d.department_id AS DepartmentId,\n d.department_name AS DepartmentName\n FROM employees e\n JOIN departments d ON e.department_id = d.department_id\n WHERE e.department_id = :dept\";\n\nvar employees = conn.Query\u003cEmployee, Department, Employee>(\n sql,\n (emp, dept) => { emp.Department = dept; return emp; },\n new { dept = 60 },\n splitOn: \"DepartmentId\"\n).ToList();\n```\n\n---\n\n## Async Support\n\n```csharp\nusing var conn = new OracleConnection(connStr);\n\nvar employees = await conn.QueryAsync\u003cEmployee>(\n \"SELECT employee_id AS EmployeeId, last_name AS LastName, salary AS Salary \" +\n \"FROM employees WHERE department_id = :dept\",\n new { dept = 60 }\n);\n\nawait conn.ExecuteAsync(\n \"UPDATE employees SET salary = :sal WHERE employee_id = :id\",\n new { sal = 9500m, id = 100 }\n);\n```\n\n---\n\n## Column Name Mapping\n\nOracle returns column names in UPPERCASE. Dapper maps by column alias to property name (case-insensitive). Always alias Oracle columns to match C# property names:\n\n```csharp\n// Without alias — may not map correctly\n\"SELECT EMPLOYEE_ID, LAST_NAME FROM employees\"\n\n// With alias — maps reliably\n\"SELECT employee_id AS EmployeeId, last_name AS LastName FROM employees\"\n```\n\nAlternatively, use `Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;` or a custom type map.\n\n---\n\n## Dependency Injection (ASP.NET Core)\n\n```csharp\n// Program.cs\nbuilder.Services.AddScoped\u003cIDbConnection>(_ =>\n new OracleConnection(builder.Configuration.GetConnectionString(\"OracleDb\")));\n\n// appsettings.json\n{\n \"ConnectionStrings\": {\n \"OracleDb\": \"User Id=hr;Password=password;Data Source=localhost:1521/freepdb1;\"\n }\n}\n\n// EmployeeRepository.cs\npublic class EmployeeRepository\n{\n private readonly IDbConnection _conn;\n\n public EmployeeRepository(IDbConnection conn) => _conn = conn;\n\n public Task\u003cIEnumerable\u003cEmployee>> GetByDeptAsync(int deptId) =>\n _conn.QueryAsync\u003cEmployee>(\n \"SELECT employee_id AS EmployeeId, last_name AS LastName \" +\n \"FROM employees WHERE department_id = :dept\",\n new { dept = deptId });\n}\n```\n\n---\n\n## Best Practices\n\n- **Always alias columns** to match C# property names — avoids silent mapping failures.\n- **Use `DynamicParameters`** for OUT parameters and PL/SQL procedure calls.\n- **Use async overloads** (`QueryAsync`, `ExecuteAsync`) in ASP.NET Core.\n- **Reuse `OracleConnection`** — Dapper does not pool connections itself; rely on ODP.NET's built-in pool.\n- **Pass `transaction:`** to every Dapper call when inside a transaction.\n\n---\n\n## Common Mistakes\n\n| Mistake | Problem | Fix |\n|---------|---------|-----|\n| No column aliases | Oracle UPPERCASE names don't map to camelCase C# properties | Always alias: `employee_id AS EmployeeId` |\n| Forgetting `transaction:` param | Operations run outside the transaction | Pass `transaction: txn` to every call |\n| `@param` instead of `:param` | ODP.NET uses `:` prefix | Use `:paramName` syntax |\n| `QuerySingle` on 0 rows | Throws `InvalidOperationException` | Use `QuerySingleOrDefault` |\n| Not opening connection | `InvalidOperationException: Connection is not open` | Call `conn.Open()` or `await conn.OpenAsync()` |\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- All patterns work on Oracle 19c+.\n- Oracle 26ai native `BOOLEAN` maps to C# `bool` via standard ODP.NET binding.\n\n## Sources\n\n- [Dapper GitHub](https://github.com/DapperLib/Dapper)\n- [Dapper Documentation](https://www.learndapper.com/)\n- [ODP.NET Managed Driver](https://docs.oracle.com/en/database/oracle/oracle-database/19/odpnt/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8870,"content_sha256":"bfbfa92c6374d7852ea6c915a8de28f7297f6d1f2adff6a1c2436b4ccfb6f1b9"},{"filename":"frameworks/gorm-oracle.md","content":"# GORM + Oracle Database\n\n## Overview\n\nGORM is the most popular Go ORM. Oracle support is community-maintained rather than first-party. A current community dialect is `github.com/godoes/gorm-oracle`. Its own README notes that it is not recommended for production use, so teams that need production-grade Oracle support often use `godror` with `database/sql` directly instead of a GORM dialect.\n\n```bash\ngo get gorm.io/gorm\ngo get github.com/godoes/gorm-oracle\n```\n\nCommunity GORM Oracle dialects vary in implementation details and support level. Validate behavior against your exact version before relying on features like schema migration or auto-generated keys in production.\n\n---\n\n## Connecting\n\n```go\npackage main\n\nimport (\n \"time\"\n\n oracle \"github.com/godoes/gorm-oracle\"\n \"gorm.io/gorm\"\n \"gorm.io/gorm/logger\"\n)\n\nfunc main() {\n options := map[string]string{\n \"CONNECTION TIMEOUT\": \"30\",\n }\n\n dsn := oracle.BuildUrl(\"localhost\", \"1521\", \"freepdb1\", \"hr\", \"password\", options)\n\n // Alternative TNS alias or descriptor forms can be supplied directly in Config.DSN\n // dsn := `user=\"hr\" password=\"password\" connectString=\"mydb_high\"`\n\n dialector := oracle.New(oracle.Config{DSN: dsn})\n db, err := gorm.Open(dialector, &gorm.Config{\n Logger: logger.Default.LogMode(logger.Info), // SQL logging\n })\n if err != nil {\n panic(\"failed to connect: \" + err.Error())\n }\n\n // Configure pool\n sqlDB, _ := db.DB()\n sqlDB.SetMaxOpenConns(20)\n sqlDB.SetMaxIdleConns(5)\n sqlDB.SetConnMaxLifetime(30 * time.Minute)\n}\n```\n\n---\n\n## Models\n\n```go\nimport \"gorm.io/gorm\"\n\n// GORM uses struct tags to map to Oracle columns\ntype Department struct {\n DepartmentID uint `gorm:\"column:DEPARTMENT_ID;primaryKey\"`\n DepartmentName string `gorm:\"column:DEPARTMENT_NAME;not null;size:30\"`\n Employees []Employee `gorm:\"foreignKey:DepartmentID\"`\n}\n\nfunc (Department) TableName() string { return \"HR.DEPARTMENTS\" }\n\ntype Employee struct {\n EmployeeID uint `gorm:\"column:EMPLOYEE_ID;primaryKey;autoIncrement:false\"`\n LastName string `gorm:\"column:LAST_NAME;not null;size:25\"`\n Email string `gorm:\"column:EMAIL;not null;size:25;uniqueIndex\"`\n Salary float64 `gorm:\"column:SALARY\"`\n HireDate time.Time `gorm:\"column:HIRE_DATE\"`\n DepartmentID uint `gorm:\"column:DEPARTMENT_ID\"`\n Department Department `gorm:\"foreignKey:DepartmentID\"`\n DeletedAt gorm.DeletedAt `gorm:\"column:DELETED_AT;index\"` // soft delete\n}\n\nfunc (Employee) TableName() string { return \"HR.EMPLOYEES\" }\n```\n\n### Oracle Sequence for Primary Key\n\nGORM's `autoIncrement` doesn't map to Oracle sequences. Use a `BeforeCreate` hook:\n\n```go\nfunc (e *Employee) BeforeCreate(tx *gorm.DB) error {\n var id int64\n tx.Raw(\"SELECT employees_seq.NEXTVAL FROM DUAL\").Scan(&id)\n e.EmployeeID = uint(id)\n return nil\n}\n```\n\n### LOB Fields\n\n```go\ntype EmployeeDoc struct {\n DocID uint `gorm:\"column:DOC_ID;primaryKey\"`\n Resume string `gorm:\"column:RESUME;type:CLOB\"`\n Photo []byte `gorm:\"column:PHOTO;type:BLOB\"`\n}\n```\n\n---\n\n## CRUD Operations\n\n### Create\n\n```go\nemp := Employee{\n LastName: \"Smith\",\n Email: \"[email protected]\",\n Salary: 7500,\n DepartmentID: 60,\n}\nresult := db.Create(&emp)\nif result.Error != nil {\n log.Fatal(result.Error)\n}\nfmt.Println(\"Inserted:\", emp.EmployeeID)\n```\n\n### Read\n\n```go\n// Find by PK\nvar emp Employee\ndb.First(&emp, 100)\n\n// Find all with conditions\nvar employees []Employee\ndb.Where(\"salary > ? AND department_id = ?\", 5000, 60).\n Order(\"last_name ASC\").\n Find(&employees)\n\n// Preload association\ndb.Preload(\"Department\").Find(&employees)\n\n// Specific columns\ndb.Select(\"EMPLOYEE_ID\", \"LAST_NAME\", \"SALARY\").\n Where(\"department_id = ?\", 60).\n Find(&employees)\n```\n\n### Update\n\n```go\n// Update single field\ndb.Model(&Employee{}).Where(\"employee_id = ?\", 100).Update(\"SALARY\", 9500)\n\n// Update multiple fields\ndb.Model(&emp).Updates(Employee{Salary: 9500, DepartmentID: 10})\n\n// Update with map (avoids zero-value issue)\ndb.Model(&Employee{EmployeeID: 100}).Updates(map[string]interface{}{\n \"SALARY\": 9500,\n \"DEPARTMENT_ID\": 10,\n})\n```\n\n### Delete\n\n```go\n// Delete by PK (soft delete if DeletedAt field exists)\ndb.Delete(&Employee{}, 100)\n\n// Hard delete (bypasses soft delete)\ndb.Unscoped().Delete(&Employee{}, 100)\n\n// Bulk delete\ndb.Where(\"department_id = ?\", 999).Delete(&Employee{})\n```\n\n---\n\n## Raw SQL\n\n```go\n// Raw SELECT\nvar employees []Employee\ndb.Raw(\n \"SELECT employee_id, last_name, salary FROM employees WHERE department_id = :dept AND salary > :minSal\",\n map[string]interface{}{\"dept\": 60, \"minSal\": 5000},\n).Scan(&employees)\n\n// Exec (DML)\ndb.Exec(\"UPDATE employees SET salary = :sal WHERE employee_id = :id\",\n map[string]interface{}{\"sal\": 9500, \"id\": 100})\n\n// PL/SQL\ndb.Exec(\"BEGIN hr.update_salary(:id, :sal); END;\",\n map[string]interface{}{\"id\": 100, \"sal\": 9500})\n```\n\n---\n\n## Transactions\n\n```go\n// Manual transaction\ntx := db.Begin()\ndefer func() {\n if r := recover(); r != nil {\n tx.Rollback()\n }\n}()\n\nif err := tx.Error; err != nil {\n log.Fatal(err)\n}\n\nif err := tx.Model(&Employee{}).Where(\"employee_id = ?\", 100).\n Update(\"SALARY\", 9500).Error; err != nil {\n tx.Rollback()\n log.Fatal(err)\n}\n\ntx.Commit()\n\n// Closure transaction (recommended)\nerr := db.Transaction(func(tx *gorm.DB) error {\n if err := tx.Model(&Employee{}).Where(\"employee_id = ?\", 100).\n Update(\"SALARY\", 9500).Error; err != nil {\n return err // auto-rollback\n }\n if err := tx.Create(&Employee{LastName: \"New\", Email: \"[email protected]\"}).Error; err != nil {\n return err\n }\n return nil // auto-commit\n})\n```\n\n---\n\n## Pagination\n\n```go\ntype PaginatedResult struct {\n Employees []Employee\n Total int64\n Page int\n PageSize int\n}\n\nfunc GetEmployees(db *gorm.DB, page, pageSize int) PaginatedResult {\n var employees []Employee\n var total int64\n\n db.Model(&Employee{}).Count(&total)\n db.Offset((page - 1) * pageSize).Limit(pageSize).\n Order(\"last_name ASC\").Find(&employees)\n\n return PaginatedResult{\n Employees: employees,\n Total: total,\n Page: page,\n PageSize: pageSize,\n }\n}\n```\n\n---\n\n## Scopes (Reusable Query Conditions)\n\n```go\nfunc HighEarners(minSal float64) func(*gorm.DB) *gorm.DB {\n return func(db *gorm.DB) *gorm.DB {\n return db.Where(\"salary > ?\", minSal)\n }\n}\n\nfunc InDepartment(deptID uint) func(*gorm.DB) *gorm.DB {\n return func(db *gorm.DB) *gorm.DB {\n return db.Where(\"department_id = ?\", deptID)\n }\n}\n\n// Usage\nvar employees []Employee\ndb.Scopes(HighEarners(5000), InDepartment(60)).Find(&employees)\n```\n\n---\n\n## Auto Migrate\n\n```go\n// Creates or updates tables to match struct definitions\n// Use with caution — does not drop columns or rename\ndb.AutoMigrate(&Employee{}, &Department{})\n\n// Better: use SQL migration files for production\n```\n\n---\n\n## Best Practices\n\n- **Use `BeforeCreate` hooks** for Oracle sequence-based PKs — do not rely on `autoIncrement`.\n- **Use `db.Transaction(func(tx) error {...})`** over manual `Begin/Commit/Rollback`.\n- **Use `map[string]interface{}`** in `Updates` to avoid GORM skipping zero-value fields.\n- **Set `TableName()`** on every model to use explicit Oracle schema-qualified names.\n- **Use `Preload`** for associations rather than `Joins` for cleaner N+1 avoidance.\n- **Never use `AutoMigrate` in production** — manage schema changes with explicit DDL scripts.\n\n---\n\n## Common Mistakes\n\n| Mistake | Problem | Fix |\n|---------|---------|-----|\n| Relying on `autoIncrement` | Dialect support varies; generated DDL may not match Oracle expectations | Use `BeforeCreate` + `NEXTVAL` unless you have verified identity-column behavior in your exact dialect version |\n| `Updates(struct{})` with zero values | GORM skips zero-value fields | Use `Updates(map[string]interface{}{...})` |\n| Missing `TableName()` | GORM generates wrong table name | Always implement `TableName()` |\n| `First` on empty result | Returns `gorm.ErrRecordNotFound` panic | Check `result.Error == gorm.ErrRecordNotFound` |\n| `AutoMigrate` in production | Alters schema unexpectedly | Use controlled DDL migrations |\n| Missing Instant Client in PATH | godror panics at startup | Set `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` |\n\n---\n\n## Oracle Version Notes (19c vs 26ai)\n\n- All patterns work on Oracle 19c+.\n- Oracle 26ai adds native `BOOLEAN` and modern `IDENTITY` support in the database, but GORM Oracle dialect behavior depends on the specific community driver version you choose.\n\n## Sources\n\n- [GORM Documentation](https://gorm.io/docs/)\n- [GORM Oracle Driver (godoes/gorm-oracle)](https://github.com/godoes/gorm-oracle)\n- [godror GitHub](https://github.com/godror/godror)\n- [Oracle Instant Client](https://www.oracle.com/database/technologies/instant-client.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8988,"content_sha256":"1c4c2df543dc78f53c2d3640dd1ab9af7b9002d690f6637ae0b6fe40446f8d45"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Oracle Database Skills","type":"text"}]},{"type":"paragraph","content":[{"text":"This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use This Domain","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Start with the routing table below.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read only the specific file or category you need.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Directory Structure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"db/\n├── admin/\n├── agent/\n├── appdev/\n├── architecture/\n├── containers/\n├── design/\n├── devops/\n├── features/\n├── frameworks/\n├── migrations/\n├── monitoring/\n├── ords/\n├── performance/\n├── plsql/\n├── security/\n├── sql-dev/\n└── sqlcl/","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Category Routing","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Topic","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Directory","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Backup, recovery, RMAN, Data Guard, redo/undo logs, users","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/admin/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/agent/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JDBC, pooling, JSON, XML, spatial, Oracle Text, transactions, MLE, language drivers","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/appdev/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RAC, Multitenant, Exadata, In-Memory, OCI database services, Data Guard architecture","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/architecture/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OCR database-category container images and pull guidance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/containers/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ERD, data modeling, partitioning, tablespaces","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/design/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Schema migrations, online operations, edition-based redefinition, testing, version control","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/devops/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AQ, DBMS_SCHEDULER, materialized views, DBLinks, APEX, vector search, SELECT AI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/features/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SQLAlchemy, Django, Pandas, Spring JPA, MyBatis, TypeORM, Sequelize, Dapper, GORM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/frameworks/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrations from PostgreSQL, MySQL, SQL Server, MongoDB, Snowflake, and more","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/migrations/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Alert log, ADR, health monitor, space management, top SQL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/monitoring/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ORDS architecture, installation, REST design, authentication, monitoring","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/ords/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AWR, ASH, explain plan, indexes, optimizer stats, wait events, memory","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/performance/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Package design, error handling, performance, collections, cursors, debugging","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/plsql/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Privileges, VPD, masking, auditing, encryption, network security","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/security/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SQL tuning, SQL patterns, dynamic SQL, injection avoidance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/sql-dev/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SQLcl basics, scripting, Liquibase, formatting, DDL generation, data loading, MCP server, scheduler daemon, AWR, background jobs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"db/sqlcl/","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Starting Points","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/sqlcl/sqlcl-mcp-server.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/migrations/migration-assessment.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/performance/explain-plan.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/plsql/plsql-package-design.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/devops/schema-migrations.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/agent/schema-discovery.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"db/containers/container-selection-matrix.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Multi-Step Flows","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Recommended Sequence","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Diagnose a slow query","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"explain-plan","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"wait-events","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"optimizer-stats","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"awr-reports","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan a migration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"migration-assessment","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"oracle-migration-tools","type":"text","marks":[{"type":"code_inline"}]},{"text":" → source-specific ","type":"text"},{"text":"migrate-*.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"migration-cutover-strategy","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Build RAG on Oracle Database","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai-profiles","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"vector-search","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"dbms-vector","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Perform agent-safe schema change","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"schema-discovery","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"destructive-op-guards","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"idempotency-patterns","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"schema-migrations","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Set up AI-driven database access via MCP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sqlcl-basics","type":"text","marks":[{"type":"code_inline"}]},{"text":" (save connections) → ","type":"text"},{"text":"security/privilege-management","type":"text","marks":[{"type":"code_inline"}]},{"text":" (least-privilege user) → ","type":"text"},{"text":"sqlcl-mcp-server","type":"text","marks":[{"type":"code_inline"}]},{"text":" (configure + start)","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"db","author":"@skillopedia","source":{"stars":638,"repo_name":"skills","origin_url":"https://github.com/oracle/skills/blob/HEAD/db/SKILL.md","repo_owner":"oracle","body_sha256":"3796b29eaf84300629936c3d7f7cad936f9561baab4c22dcc25a7bbfdd2863bf","cluster_key":"62385c1439cce0f727a8f4d725826b7cbd5807d0bb6105ca601145029fc6ec7d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"oracle/skills/db/SKILL.md","attachments":[{"id":"7c72c6b4-4953-5d7d-9b3b-4d81818226e4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7c72c6b4-4953-5d7d-9b3b-4d81818226e4/attachment.md","path":"admin/backup-recovery.md","size":16150,"sha256":"1722ec58963165adb3c762fedf281a81307aaba2e13843e2a524e45c67514d5d","contentType":"text/markdown; charset=utf-8"},{"id":"173979f0-3957-5912-8642-7a222c61522b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/173979f0-3957-5912-8642-7a222c61522b/attachment.md","path":"admin/dataguard.md","size":16074,"sha256":"a1b06a57169f05bc7550c3e30ded0f3d7bd502abf3315ea1e96c14a2f8d6afd3","contentType":"text/markdown; charset=utf-8"},{"id":"b5478852-3470-5f51-bd7c-4566bc9d01b5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b5478852-3470-5f51-bd7c-4566bc9d01b5/attachment.md","path":"admin/redo-log-management.md","size":18310,"sha256":"e1417dff0b6e23916c3f74fd784f3f3b2501157973d88c6b92ed1a3947e3584f","contentType":"text/markdown; charset=utf-8"},{"id":"b2217813-e0a7-5aa4-be50-9b28acbe7cea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2217813-e0a7-5aa4-be50-9b28acbe7cea/attachment.md","path":"admin/rman-basics.md","size":17842,"sha256":"8ba82be3f0c496c05e422afe423939c35c304b819887da09a0fdf6fabe617df1","contentType":"text/markdown; charset=utf-8"},{"id":"48199eab-a9d2-566b-aed4-45363b8d40f4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/48199eab-a9d2-566b-aed4-45363b8d40f4/attachment.md","path":"admin/undo-management.md","size":14371,"sha256":"bb91494b52b23860d4e15dba5611ed7eb086567a949d2667f2e42e6878278fe8","contentType":"text/markdown; charset=utf-8"},{"id":"98dc8c4d-3960-5404-9b3c-e29b28655dad","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98dc8c4d-3960-5404-9b3c-e29b28655dad/attachment.md","path":"admin/user-management.md","size":19782,"sha256":"56cc56c8d2eb923366c8cd201b76f5932e5fb233cafdecac8c5a2a3dc61d6381","contentType":"text/markdown; charset=utf-8"},{"id":"b69c2849-6ddb-5860-aa35-28dff0b0a67e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b69c2849-6ddb-5860-aa35-28dff0b0a67e/attachment.md","path":"agent/client-identification.md","size":10944,"sha256":"2fd3a037d2662ae5962cadf1d5e91071c3e9ab0bbbfbbb8be6d100b9039b98e0","contentType":"text/markdown; charset=utf-8"},{"id":"30d25fb5-b953-522e-a8ae-14ce4be9fbb6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/30d25fb5-b953-522e-a8ae-14ce4be9fbb6/attachment.md","path":"agent/destructive-op-guards.md","size":10846,"sha256":"8cbae42343d9775c0932b2a05695ccf2deb70e9d0718a4c9928bc6b8ef3a32e4","contentType":"text/markdown; charset=utf-8"},{"id":"3f7ff416-1d3a-5032-bf74-7137184a3f3e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3f7ff416-1d3a-5032-bf74-7137184a3f3e/attachment.md","path":"agent/idempotency-patterns.md","size":12611,"sha256":"b5d8c08b72ef3b4b364e48f84ad08caa11e50f59255b78fbd139f989994a63f0","contentType":"text/markdown; charset=utf-8"},{"id":"aaaa70cc-aaa8-56cd-922c-5afe0895f77a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aaaa70cc-aaa8-56cd-922c-5afe0895f77a/attachment.md","path":"agent/intent-disambiguation.md","size":12509,"sha256":"a7bcc67a7f9c16c9f45b7d43c891d218e2d6ef5cf9686a04c1ea1e4dc265e049","contentType":"text/markdown; charset=utf-8"},{"id":"9b2eaf1b-8d1f-58b7-95e0-1eef96546206","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9b2eaf1b-8d1f-58b7-95e0-1eef96546206/attachment.md","path":"agent/nl-to-sql-patterns.md","size":13488,"sha256":"148efa4782ffedb79f7b89eb62cc853d1583f7cd6748d64a60d670b50999d82a","contentType":"text/markdown; charset=utf-8"},{"id":"48d495fd-4237-5d59-bdb0-045485da6883","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/48d495fd-4237-5d59-bdb0-045485da6883/attachment.md","path":"agent/ora-error-catalog.md","size":27929,"sha256":"35cf61cd8a1f3b62a886c3a401e1cb285988ff4f7503e00a49fe2858fa81dd25","contentType":"text/markdown; charset=utf-8"},{"id":"88c2be20-36de-5872-b38d-4fd8c7ba667b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/88c2be20-36de-5872-b38d-4fd8c7ba667b/attachment.md","path":"agent/safe-dml-patterns.md","size":15154,"sha256":"063fb2ec205b53c95e390c6304d46588c8a5f55313299a7de0322c25716253cb","contentType":"text/markdown; charset=utf-8"},{"id":"ec922874-6be9-527e-b5f1-b5736222c480","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec922874-6be9-527e-b5f1-b5736222c480/attachment.md","path":"agent/schema-discovery.md","size":19192,"sha256":"071bc0e006c5017fac615e666aa11ae8f8f68dc367438e5fe133984c4ad6ce53","contentType":"text/markdown; charset=utf-8"},{"id":"5f77835a-f8f1-57ae-8eda-9d5e086aa381","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5f77835a-f8f1-57ae-8eda-9d5e086aa381/attachment.md","path":"appdev/connection-pooling.md","size":14911,"sha256":"971d7333e8e181fa346bec07587c1d6535a7d4298c22bc82c5526ec373fa70be","contentType":"text/markdown; charset=utf-8"},{"id":"e4da115a-3d33-5cef-ab98-ddb10b8be26c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e4da115a-3d33-5cef-ab98-ddb10b8be26c/attachment.md","path":"appdev/dotnet-oracle.md","size":9573,"sha256":"51bdebe0a85b661dbf09f58e0aeaaf4e7b3fcb8761ebf84e1d2a3e0e08b722be","contentType":"text/markdown; charset=utf-8"},{"id":"e93a5393-d952-5a1e-9864-329a66e4f2d9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e93a5393-d952-5a1e-9864-329a66e4f2d9/attachment.md","path":"appdev/golang-oracle.md","size":8725,"sha256":"102e50b541cf8843a43840d6bab628f12086eb906290a9449c00f5cbc056fc1e","contentType":"text/markdown; charset=utf-8"},{"id":"74c5c25b-2a59-5a1d-9430-f6dfeacada29","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/74c5c25b-2a59-5a1d-9430-f6dfeacada29/attachment.md","path":"appdev/java-oracle-jdbc.md","size":13630,"sha256":"682ccc84eff4af00ccae0ce72bc3a67974b3c408efa405be6a4d5643e5f97515","contentType":"text/markdown; charset=utf-8"},{"id":"0c016e0a-8052-505d-8acd-d3d295c56e0c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0c016e0a-8052-505d-8acd-d3d295c56e0c/attachment.md","path":"appdev/json-in-oracle.md","size":18533,"sha256":"9bd283436393a9450a470b5244787e7a7ffb3929959e8254e446e435fa092e01","contentType":"text/markdown; charset=utf-8"},{"id":"d37929ec-094f-5f03-b24e-017bd1b703a6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d37929ec-094f-5f03-b24e-017bd1b703a6/attachment.md","path":"appdev/locking-concurrency.md","size":19109,"sha256":"9db34d0cb4d0375723dc7273d0bf1af040f176448c1132050a6bfd895dc1d849","contentType":"text/markdown; charset=utf-8"},{"id":"98e7d1af-6c76-5be8-a093-65f0fa28d956","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98e7d1af-6c76-5be8-a093-65f0fa28d956/attachment.md","path":"appdev/mle-javascript-in-database.md","size":29560,"sha256":"d51b55bb6262b5acf98b80d7785df82b228bee622f9ea6f9cb7d9c7b6c0d38ee","contentType":"text/markdown; charset=utf-8"},{"id":"30717dee-b0d9-541f-8596-e1b380cd1c5b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/30717dee-b0d9-541f-8596-e1b380cd1c5b/attachment.md","path":"appdev/nodejs-oracledb.md","size":8332,"sha256":"c61611e4534154d5c48f6039a9d1d495be95b6f6d6b9631974b2af5e9e15217e","contentType":"text/markdown; charset=utf-8"},{"id":"03db4701-b419-518a-96aa-ef21e4935858","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/03db4701-b419-518a-96aa-ef21e4935858/attachment.md","path":"appdev/oracle-text.md","size":20514,"sha256":"f017dcdb7df4f4eabfb6e4200067891eb87db9496ca9b878764fb38fbfe05934","contentType":"text/markdown; charset=utf-8"},{"id":"b2def1ba-6969-58af-b1e6-d19c241555b0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2def1ba-6969-58af-b1e6-d19c241555b0/attachment.md","path":"appdev/python-oracledb.md","size":9368,"sha256":"8fa9008b87a219000f7e3e13b819c4b2ba5afb986d547cd992a091dd445e50e2","contentType":"text/markdown; charset=utf-8"},{"id":"eae60069-0915-5300-9d4d-ca764ee1f338","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eae60069-0915-5300-9d4d-ca764ee1f338/attachment.md","path":"appdev/sequences-identity.md","size":16951,"sha256":"ccd342add13edac2ec0a4f9b61c41e2bf46562c21a8d5144fc76eb47617364e7","contentType":"text/markdown; charset=utf-8"},{"id":"75090cf9-c246-5ee0-9476-a0cdb87d9849","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/75090cf9-c246-5ee0-9476-a0cdb87d9849/attachment.md","path":"appdev/spatial-data.md","size":18030,"sha256":"94a1e12d54f2f21f82e7ec48643c4b38af07192b19e8f128582dadabb740e0b2","contentType":"text/markdown; charset=utf-8"},{"id":"e3b039c1-9208-59fa-b929-93a07ec5eaa4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e3b039c1-9208-59fa-b929-93a07ec5eaa4/attachment.md","path":"appdev/sql-property-graph.md","size":22330,"sha256":"ab5cc309cd58b44186765a8c08f13d07c848899505256e0da6af3b56656fb3f3","contentType":"text/markdown; charset=utf-8"},{"id":"4ac6b4af-3cee-59a9-8c66-1f40260c27b4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4ac6b4af-3cee-59a9-8c66-1f40260c27b4/attachment.md","path":"appdev/transaction-management.md","size":17201,"sha256":"25c5ea3b1756e0777b2e2c2b9564ba76c7fe7d857d6d727eccde925806fde7f3","contentType":"text/markdown; charset=utf-8"},{"id":"f14e3828-d6e9-5949-b01a-bfe3b4ab91ed","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f14e3828-d6e9-5949-b01a-bfe3b4ab91ed/attachment.md","path":"appdev/xml-in-oracle.md","size":17255,"sha256":"fb1699b7c39d0ed815cc8a60eee74d0e8597d0504a427f31675bc85393c4b07c","contentType":"text/markdown; charset=utf-8"},{"id":"9a8cde07-d85b-5a82-b017-6dd04b0bc074","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9a8cde07-d85b-5a82-b017-6dd04b0bc074/attachment.md","path":"architecture/dataguard.md","size":23059,"sha256":"551519a3213f7de1f5c9d0f083cd8d474e0632848c0571628d1fe218e1eb72cf","contentType":"text/markdown; charset=utf-8"},{"id":"750dc953-ec7b-5ff9-9924-b92bc5bbecfa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/750dc953-ec7b-5ff9-9924-b92bc5bbecfa/attachment.md","path":"architecture/exadata-features.md","size":22175,"sha256":"b42ad661e54fbd3d60c63c4d16f626808fd5b5cc84a6c34dce51c2db0eff40a6","contentType":"text/markdown; charset=utf-8"},{"id":"2eaefa94-cfe1-5824-8c79-6c6c63304978","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2eaefa94-cfe1-5824-8c79-6c6c63304978/attachment.md","path":"architecture/inmemory-column-store.md","size":21883,"sha256":"81601de51765e16fd1405c80a84471a81565f7edffe479d5dc715725809353a3","contentType":"text/markdown; charset=utf-8"},{"id":"88e5ad1e-dc76-5467-9fd0-f782003a2bc0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/88e5ad1e-dc76-5467-9fd0-f782003a2bc0/attachment.md","path":"architecture/multitenant.md","size":19222,"sha256":"61ba841ef2169dce9046f72f89991f54b54f959060f233e49994624fe0ce4581","contentType":"text/markdown; charset=utf-8"},{"id":"a1223d40-51c5-58ac-bb7a-87bb06bd8e5c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a1223d40-51c5-58ac-bb7a-87bb06bd8e5c/attachment.md","path":"architecture/oracle-cloud-oci.md","size":18609,"sha256":"4abe62c40c349bd66fc7153dff12e50292c414171083b4ebe43280d81ae33a3c","contentType":"text/markdown; charset=utf-8"},{"id":"116e8e52-0626-5295-9248-3b674af2544a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/116e8e52-0626-5295-9248-3b674af2544a/attachment.md","path":"architecture/rac-concepts.md","size":23604,"sha256":"080afd11eef6654cf87696cfaf70c15344d0ca6f470667f0619e1e9dd892e477","contentType":"text/markdown; charset=utf-8"},{"id":"e8852ba0-1fa6-5071-9ce9-abb457fdd875","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e8852ba0-1fa6-5071-9ce9-abb457fdd875/attachment.md","path":"containers/SKILLS.md","size":2037,"sha256":"05730cf5b6b66ac4a8cebd1c563bc0c89deb2f3f32ea171f2b5a7e2c849fba63","contentType":"text/markdown; charset=utf-8"},{"id":"7cffb51d-702d-5b0d-954f-960f32692b21","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7cffb51d-702d-5b0d-954f-960f32692b21/attachment.md","path":"containers/adb-free.md","size":2429,"sha256":"585552aae246351979bf0da8d0d5c040f3af963ebb7213c5fac5a4dc04b47f53","contentType":"text/markdown; charset=utf-8"},{"id":"2b6f88a1-e792-55da-8e83-5d87d4e5a74c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2b6f88a1-e792-55da-8e83-5d87d4e5a74c/attachment.md","path":"containers/cman.md","size":2405,"sha256":"98be7328f572eca3206e157d64b13e2855f266e42258fcd07b801f4eb0e368e6","contentType":"text/markdown; charset=utf-8"},{"id":"fb32bdf8-45d7-58a7-a0ef-3b30b3c830c0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fb32bdf8-45d7-58a7-a0ef-3b30b3c830c0/attachment.md","path":"containers/container-selection-matrix.md","size":6408,"sha256":"66e696b92c6b93f644fbb6387ffce8664b627e966e850efcfff855a7730f8234","contentType":"text/markdown; charset=utf-8"},{"id":"5c22b902-a23e-5b60-ad67-8217e60a2ed5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5c22b902-a23e-5b60-ad67-8217e60a2ed5/attachment.md","path":"containers/enterprise.md","size":2311,"sha256":"f7271e59cd9b67d1958fa2bf52a6a8a6e2de6a7973255283a842ba20806cfb24","contentType":"text/markdown; charset=utf-8"},{"id":"5700c466-a6f0-54ab-bc59-dc35daac6220","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5700c466-a6f0-54ab-bc59-dc35daac6220/attachment.md","path":"containers/enterprise_ru.md","size":2290,"sha256":"e7fce40b3297269b19166fa94eec603f228c3a34da912465bd2497c6523f611d","contentType":"text/markdown; charset=utf-8"},{"id":"9b018427-5751-52ca-adae-eb9c1ba9cda1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9b018427-5751-52ca-adae-eb9c1ba9cda1/attachment.md","path":"containers/free.md","size":2385,"sha256":"05a68981a2c3383e984eabdb88687f980d7e6a6cf5e793b71094cbd44912e74a","contentType":"text/markdown; charset=utf-8"},{"id":"538e5002-1608-5c82-ab9f-3b595325b040","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/538e5002-1608-5c82-ab9f-3b595325b040/attachment.md","path":"containers/graph-quickstart.md","size":2527,"sha256":"403e302d487bc573309501d88c8049da0bde4c8f9b38b81a2fceee21522f5a91","contentType":"text/markdown; charset=utf-8"},{"id":"f513817f-e3d4-5ee6-a6ad-ceccde54b550","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f513817f-e3d4-5ee6-a6ad-ceccde54b550/attachment.md","path":"containers/gsm.md","size":2298,"sha256":"befff3b7ac124450acef019f6331d4e912d5975e7ce276924f4c06c89216b6f3","contentType":"text/markdown; charset=utf-8"},{"id":"a54ce04d-d3ee-514a-b120-4744432937d7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a54ce04d-d3ee-514a-b120-4744432937d7/attachment.md","path":"containers/gsm_ru.md","size":2365,"sha256":"af640a7ad43ac50760f7ec271903a9d8f8b18bb91d6a6ed619ca565f2ef21c59","contentType":"text/markdown; charset=utf-8"},{"id":"ce2646bd-33f7-5bf0-a98f-b357f29b8116","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce2646bd-33f7-5bf0-a98f-b357f29b8116/attachment.md","path":"containers/instantclient.md","size":2279,"sha256":"ca9e5aabbc075a3300504c25c8bfb724369388a1841ee1ece8365cbe595c349f","contentType":"text/markdown; charset=utf-8"},{"id":"08f176b5-251b-52f6-86d7-e04e09531cd1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/08f176b5-251b-52f6-86d7-e04e09531cd1/attachment.md","path":"containers/microtx-ee-console.md","size":2542,"sha256":"338329a01c13247d8c705f103fc74232483923bba900aecc05674498ec95021c","contentType":"text/markdown; charset=utf-8"},{"id":"4aa1307c-4278-5d1b-9c78-2c80d86ef39b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4aa1307c-4278-5d1b-9c78-2c80d86ef39b/attachment.md","path":"containers/microtx-ee-coordinator.md","size":2745,"sha256":"c510850a37c7dd057a78e35ad4ed7bdc20b8f0c0a26190a6915f01a981a40b5e","contentType":"text/markdown; charset=utf-8"},{"id":"c07fd34a-5471-5e95-8af0-bc85dbb2c459","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c07fd34a-5471-5e95-8af0-bc85dbb2c459/attachment.md","path":"containers/observability-exporter.md","size":2366,"sha256":"982a374d1573da6deaaf9e2362bb876ddb8b3a440282abd0a46caf0d03f8bd7e","contentType":"text/markdown; charset=utf-8"},{"id":"87f44588-0144-5206-b45a-78429bbc6b10","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/87f44588-0144-5206-b45a-78429bbc6b10/attachment.md","path":"containers/operator.md","size":2469,"sha256":"64d1d992eec7fddc648cf7ab1015b32010435d6e02445c37bdb0d8a35a7d2a3c","contentType":"text/markdown; charset=utf-8"},{"id":"d3f32ead-9cca-5834-87cf-ba935956c478","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3f32ead-9cca-5834-87cf-ba935956c478/attachment.md","path":"containers/ords.md","size":2450,"sha256":"bf8933424e307eae14971615e2b2de1d0d4dcb1698c5cb9f39fa26df78f3c1e2","contentType":"text/markdown; charset=utf-8"},{"id":"6dc1b176-6fff-5c56-b6d1-3aee2c720fdb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6dc1b176-6fff-5c56-b6d1-3aee2c720fdb/attachment.md","path":"containers/otmm.md","size":2489,"sha256":"29cffc03d3bf0cbc58391ba1c118471b74e19f4373d4a8d3d8b93944a388188d","contentType":"text/markdown; charset=utf-8"},{"id":"6752d6d0-8e68-5bd3-9dfe-940c77b64781","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6752d6d0-8e68-5bd3-9dfe-940c77b64781/attachment.md","path":"containers/private-ai.md","size":2654,"sha256":"5aad180063f8697b215c572c7b1c2f6f169d73641ffeea3116379a2540096770","contentType":"text/markdown; charset=utf-8"},{"id":"f11d08e9-d682-5fce-bab9-2f2174c394f6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f11d08e9-d682-5fce-bab9-2f2174c394f6/attachment.md","path":"containers/rac.md","size":2595,"sha256":"5e03dc1a84921606acefea0a67bb30f2cdaba624a57a1527592f821cdfc5a528","contentType":"text/markdown; charset=utf-8"},{"id":"58a8e5be-0604-5dd7-a966-54a391d22668","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/58a8e5be-0604-5dd7-a966-54a391d22668/attachment.md","path":"containers/rac_ru.md","size":2609,"sha256":"570d7c7f28426f67d917001ac53d577361db6b4b551f747b4ec8909f43c8677f","contentType":"text/markdown; charset=utf-8"},{"id":"09306608-6658-5e6e-99e5-b54ace98ed73","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/09306608-6658-5e6e-99e5-b54ace98ed73/attachment.md","path":"containers/sqlcl.md","size":2459,"sha256":"0574840ab6eb76097855fbf29b9785069032b660e4e840ab9e834e56732213a4","contentType":"text/markdown; charset=utf-8"},{"id":"1c899c4f-de03-5d0c-823b-ceea3ebc9e1b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c899c4f-de03-5d0c-823b-ceea3ebc9e1b/attachment.md","path":"design/data-modeling.md","size":31255,"sha256":"b10b3e39281f2a1a1a7f93dcd9b626ca7700fa287f2d1a86da77588598ad1445","contentType":"text/markdown; charset=utf-8"},{"id":"8bf847bc-3593-55f0-bd04-a7d3a5f4a381","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8bf847bc-3593-55f0-bd04-a7d3a5f4a381/attachment.md","path":"design/erd-design.md","size":20366,"sha256":"24b6ee77104c462cf7e152c53241bfb92f95ed8265fa97c9d63760098c5714d0","contentType":"text/markdown; charset=utf-8"},{"id":"df2cf268-5703-523a-b2d1-189f589cdd02","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/df2cf268-5703-523a-b2d1-189f589cdd02/attachment.md","path":"design/partitioning-strategy.md","size":23130,"sha256":"29ced897b7a90ebc03ca2f5dc010ff8db3f47998b1ca408d95645eff153d074d","contentType":"text/markdown; charset=utf-8"},{"id":"e68532e5-76e4-50a6-a109-d8191d6d23e8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e68532e5-76e4-50a6-a109-d8191d6d23e8/attachment.md","path":"design/tablespace-design.md","size":23813,"sha256":"cdf66310b442d698472e76a2952200c885d9697db622c2838e623d0da1b89c54","contentType":"text/markdown; charset=utf-8"},{"id":"7c75c1e3-54c9-5015-a733-0da6dafd60e7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7c75c1e3-54c9-5015-a733-0da6dafd60e7/attachment.md","path":"devops/database-testing.md","size":25757,"sha256":"268988c0c76d3c9984caa303efe37bde812ee762cd92851655b6c713ddb638f8","contentType":"text/markdown; charset=utf-8"},{"id":"ecbfd0f5-0bd4-5e79-81fc-11e22ca8bb23","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ecbfd0f5-0bd4-5e79-81fc-11e22ca8bb23/attachment.md","path":"devops/edition-based-redefinition.md","size":16705,"sha256":"342da07fd8d5937aefffcf09bfc5214588cf0c293d01f63a7ea3c589c37d2328","contentType":"text/markdown; charset=utf-8"},{"id":"c0afa826-73ed-57e8-9e09-b0aa794999d4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0afa826-73ed-57e8-9e09-b0aa794999d4/attachment.md","path":"devops/online-operations.md","size":17740,"sha256":"c2feb44ad54cd7ab1f88019963f5a18ffbfed62e32e405828b37c5366ab1b695","contentType":"text/markdown; charset=utf-8"},{"id":"2bdb01b3-c626-55af-b292-9658d58af0e1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2bdb01b3-c626-55af-b292-9658d58af0e1/attachment.md","path":"devops/schema-migrations.md","size":30736,"sha256":"e7b5c6f2f0792453b061b89dbde21a3ea78f657f2fe56584470ea3af3be69ad9","contentType":"text/markdown; charset=utf-8"},{"id":"a7d87779-aa8f-5729-844b-f42b2fb1563c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a7d87779-aa8f-5729-844b-f42b2fb1563c/attachment.md","path":"devops/version-control-sql.md","size":21407,"sha256":"7463d35fb3be296348a082a2bf9dc4bd940e8b19c9b51aad820b3593a888c6c4","contentType":"text/markdown; charset=utf-8"},{"id":"c4965310-851f-5288-ac18-ac27eac3a10c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4965310-851f-5288-ac18-ac27eac3a10c/attachment.md","path":"features/advanced-queuing.md","size":17997,"sha256":"c81c64e49cf345e49e5cb26547ad2993a708dbe4c0020e3ab8b0d0d22bca4361","contentType":"text/markdown; charset=utf-8"},{"id":"7ab93521-2005-540f-b0dd-d7a2821239e5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ab93521-2005-540f-b0dd-d7a2821239e5/attachment.md","path":"features/ai-profiles.md","size":14728,"sha256":"d1ae026714f45ff2561842ebbe754e32f88631b2016cdb293b220a38ffedfc69","contentType":"text/markdown; charset=utf-8"},{"id":"8f3ae57c-e86f-5ef1-82a3-2ea766d20811","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8f3ae57c-e86f-5ef1-82a3-2ea766d20811/attachment.md","path":"features/database-links.md","size":16362,"sha256":"c841008987eac9223353d5d363dbe0c0af0b94e9bc1107756de461656bfb16e7","contentType":"text/markdown; charset=utf-8"},{"id":"2be86903-d3c6-5d4a-8dcd-3d46e713e30d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2be86903-d3c6-5d4a-8dcd-3d46e713e30d/attachment.md","path":"features/dbms-scheduler.md","size":22165,"sha256":"a6728ba19fb6a1270588f1331b5a3a43d145111bd1225bab46e43c9f265ff406","contentType":"text/markdown; charset=utf-8"},{"id":"1ce5533e-9cb8-536a-86ff-634ab127c566","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1ce5533e-9cb8-536a-86ff-634ab127c566/attachment.md","path":"features/dbms-vector.md","size":19086,"sha256":"ca021405a7b609d6cb995664abe80bc159610d2d2fa5924808c7756967089580","contentType":"text/markdown; charset=utf-8"},{"id":"24d319d6-e3bc-56aa-be58-5ea4cc06c45b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/24d319d6-e3bc-56aa-be58-5ea4cc06c45b/attachment.md","path":"features/materialized-views.md","size":16860,"sha256":"ffa2ab31a2116d7872caf03cf0184f49837478f56f71d307a8fefad1c315c2bf","contentType":"text/markdown; charset=utf-8"},{"id":"2becb9af-b3e1-5eae-90a0-081ead33ff52","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2becb9af-b3e1-5eae-90a0-081ead33ff52/attachment.md","path":"features/oracle-apex.md","size":4790,"sha256":"27e08f7b3ac893de9e423ce471d11ecc091cc94b18e3ab7c1a74d74f84e85c53","contentType":"text/markdown; charset=utf-8"},{"id":"9672f95e-8e15-5992-b03a-320301721ee0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9672f95e-8e15-5992-b03a-320301721ee0/attachment.md","path":"features/select-ai.md","size":21022,"sha256":"19611d0764d60a0227b4a6363b96dc81906394981f2d075adc5a366e769e2817","contentType":"text/markdown; charset=utf-8"},{"id":"99a720f8-77a9-5f49-8a03-481ab62a80a3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/99a720f8-77a9-5f49-8a03-481ab62a80a3/attachment.md","path":"features/vector-search.md","size":13541,"sha256":"d28dcdad24b94969427289f71bbe619aa031c0ce4957f3b1d33976ce7a642eb8","contentType":"text/markdown; charset=utf-8"},{"id":"9801ccab-b8ad-561c-b9de-a936ba76657e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9801ccab-b8ad-561c-b9de-a936ba76657e/attachment.md","path":"features/virtual-columns.md","size":14150,"sha256":"164d976646c348e5fae7601f233a46104464f5871454b39cf87b48c04c7dd7c6","contentType":"text/markdown; charset=utf-8"},{"id":"29352f89-65dc-5091-9e89-7ec0adb4e4dc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/29352f89-65dc-5091-9e89-7ec0adb4e4dc/attachment.md","path":"frameworks/dapper-oracle.md","size":8870,"sha256":"bfbfa92c6374d7852ea6c915a8de28f7297f6d1f2adff6a1c2436b4ccfb6f1b9","contentType":"text/markdown; charset=utf-8"},{"id":"883337e5-d7d2-5823-ad73-755f9ade8a7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/883337e5-d7d2-5823-ad73-755f9ade8a7f/attachment.md","path":"frameworks/django-oracle.md","size":21258,"sha256":"4793f8548c0a90cdf2b476da1132aba090242eebed4323de8e9397f7b37af18d","contentType":"text/markdown; charset=utf-8"},{"id":"013d6eef-aba1-58ea-8997-c7b683ac42fe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/013d6eef-aba1-58ea-8997-c7b683ac42fe/attachment.md","path":"frameworks/gorm-oracle.md","size":8988,"sha256":"1c4c2df543dc78f53c2d3640dd1ab9af7b9002d690f6637ae0b6fe40446f8d45","contentType":"text/markdown; charset=utf-8"},{"id":"316adbcd-079d-5642-b402-b610a050122e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/316adbcd-079d-5642-b402-b610a050122e/attachment.md","path":"frameworks/mybatis-oracle.md","size":9821,"sha256":"e17949434e09a2401b9dd7c2bedfb98301962ac95f13f41750720df1f4ba910b","contentType":"text/markdown; charset=utf-8"},{"id":"4dd84263-56c4-5007-a4c8-aa0399c79715","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4dd84263-56c4-5007-a4c8-aa0399c79715/attachment.md","path":"frameworks/pandas-oracle.md","size":7708,"sha256":"3fb6433f864ebb02f63504067b01e9ddf7b04e5f6ec3281b17aee315416febc9","contentType":"text/markdown; charset=utf-8"},{"id":"92829c64-d351-5c00-a56b-5cb406b768b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/92829c64-d351-5c00-a56b-5cb406b768b9/attachment.md","path":"frameworks/sequelize-oracle.md","size":8602,"sha256":"0372218abc457173832e7c6b20619790434b3a03c8e9303e6c237703d9a028d1","contentType":"text/markdown; charset=utf-8"},{"id":"0e6426aa-faab-5e8a-86c0-14539eae1ee7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e6426aa-faab-5e8a-86c0-14539eae1ee7/attachment.md","path":"frameworks/spring-data-jpa-oracle.md","size":20823,"sha256":"bc1ffdb05f34bce3358faaae1bf81a9d137d0d1c6d15e9e86226d9060dec9f25","contentType":"text/markdown; charset=utf-8"},{"id":"ace2aed6-51e9-5f4b-817d-f703d0dba405","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ace2aed6-51e9-5f4b-817d-f703d0dba405/attachment.md","path":"frameworks/sqlalchemy-oracle.md","size":22958,"sha256":"cec984bf16859c00519192aa23aa6ee76a491e86f3aebb407ecfec0bf1d981b8","contentType":"text/markdown; charset=utf-8"},{"id":"adffc1af-da6c-5bc1-817b-3ac8d2101577","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/adffc1af-da6c-5bc1-817b-3ac8d2101577/attachment.md","path":"frameworks/typeorm-oracle.md","size":27361,"sha256":"fa380a868061da1c8fc612e4b92bb5050a0c586e4acefb640dba2b076ecde243","contentType":"text/markdown; charset=utf-8"},{"id":"4b0bd295-bee8-5dfc-9ee3-7b63bc5ba204","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4b0bd295-bee8-5dfc-9ee3-7b63bc5ba204/attachment.md","path":"migrations/migrate-db2-to-oracle.md","size":20180,"sha256":"784dae4923e8b579b08b74d2718a6be7e8b8eca8b1f51a32567348e544790eda","contentType":"text/markdown; charset=utf-8"},{"id":"aab73c18-7027-5aa3-adfc-604e347fc5c9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aab73c18-7027-5aa3-adfc-604e347fc5c9/attachment.md","path":"migrations/migrate-mongodb-to-oracle.md","size":19981,"sha256":"8243c8bdd975be2cd7a9c8b2b30c6468d821a8fd4c2ed95d30b98f746f17170b","contentType":"text/markdown; charset=utf-8"},{"id":"c2f4a235-f275-5d00-9c97-cbf7346717e6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c2f4a235-f275-5d00-9c97-cbf7346717e6/attachment.md","path":"migrations/migrate-mysql-to-oracle.md","size":20357,"sha256":"f81494bb3318c6ff237b758477637cdcb889142aa3e14c0a1756f8d0ddb4fe47","contentType":"text/markdown; charset=utf-8"},{"id":"747281a9-f3f0-5f94-b31b-39bc29633890","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/747281a9-f3f0-5f94-b31b-39bc29633890/attachment.md","path":"migrations/migrate-postgres-to-oracle.md","size":20174,"sha256":"1f4c20785c0fd56c5a0a245aadff2a6e59683bf457ed86136213487cc2fe8767","contentType":"text/markdown; charset=utf-8"},{"id":"6f7c5e52-f45a-53e6-83de-9df0de77e7e0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6f7c5e52-f45a-53e6-83de-9df0de77e7e0/attachment.md","path":"migrations/migrate-redshift-to-oracle.md","size":18698,"sha256":"98ac63300926ab75d844dab859b7b97253552e15113a459367e11229f2e1f9cd","contentType":"text/markdown; charset=utf-8"},{"id":"d37d8125-50b6-5b72-870a-949aa9a11ce8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d37d8125-50b6-5b72-870a-949aa9a11ce8/attachment.md","path":"migrations/migrate-snowflake-to-oracle.md","size":19713,"sha256":"b3908b5a0bc505f2034cb3d738d19729de9073d26f0e7e6b0fc12a86956f14a4","contentType":"text/markdown; charset=utf-8"},{"id":"5fd0b276-31c1-519f-bece-575799b789c0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5fd0b276-31c1-519f-bece-575799b789c0/attachment.md","path":"migrations/migrate-sqlite-to-oracle.md","size":20131,"sha256":"10d7c6658050fdfbf21bb23d97871a636aeef7683b33fcbb83f00f7620cea0c9","contentType":"text/markdown; charset=utf-8"},{"id":"31cac394-9108-5eb7-a0d8-0d1aaaa29d65","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/31cac394-9108-5eb7-a0d8-0d1aaaa29d65/attachment.md","path":"migrations/migrate-sqlserver-to-oracle.md","size":19115,"sha256":"2b7d0711102c3b48ed653e46c247357361891ee7f6d79dab1c4c761332a7cb5c","contentType":"text/markdown; charset=utf-8"},{"id":"121b3a43-8688-55df-b6c0-a38a673cb4aa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/121b3a43-8688-55df-b6c0-a38a673cb4aa/attachment.md","path":"migrations/migrate-sybase-to-oracle.md","size":20797,"sha256":"8bd365cbb9af7c487672b1a1e739cab49a8c00dc073fb2b67c4d9cb95f8da868","contentType":"text/markdown; charset=utf-8"},{"id":"b334fe07-4945-5231-9875-2d5d030027f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b334fe07-4945-5231-9875-2d5d030027f2/attachment.md","path":"migrations/migrate-teradata-to-oracle.md","size":18843,"sha256":"659da281e3c113cd5340c09af6dd981ae4f89b2cadb664212e501f86a9b3c020","contentType":"text/markdown; charset=utf-8"},{"id":"a825bab4-7f83-53ad-94e9-c9eefb4be8ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a825bab4-7f83-53ad-94e9-c9eefb4be8ee/attachment.md","path":"migrations/migration-assessment.md","size":19905,"sha256":"6373595ec996a18dd328ab1246ed64f3c86b98f43b1f8049dd22185981e28335","contentType":"text/markdown; charset=utf-8"},{"id":"27956705-3b26-53c1-bf2b-5a7890240312","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/27956705-3b26-53c1-bf2b-5a7890240312/attachment.md","path":"migrations/migration-cutover-strategy.md","size":23314,"sha256":"11211675a20215017f8fb3da1b8f80c879f3d02f5925857d1b30bcdae1195d06","contentType":"text/markdown; charset=utf-8"},{"id":"095807eb-cb7b-5482-8dd4-728e58634ed8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/095807eb-cb7b-5482-8dd4-728e58634ed8/attachment.md","path":"migrations/migration-data-validation.md","size":22729,"sha256":"fe081a695fca095800fdc56ae5fdf4d626da1d2c9da9b7018213de9ae786299c","contentType":"text/markdown; charset=utf-8"},{"id":"d8fcf1dd-63f4-58ba-a29d-b12c1132d352","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d8fcf1dd-63f4-58ba-a29d-b12c1132d352/attachment.md","path":"migrations/oracle-migration-tools.md","size":17758,"sha256":"d25558b60411ef25662faa1c03dd1d23fb4d2dfd9e67869e1f0098276b22a42c","contentType":"text/markdown; charset=utf-8"},{"id":"c8b4368e-4b30-5cdc-bada-70f14a5c408b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c8b4368e-4b30-5cdc-bada-70f14a5c408b/attachment.md","path":"monitoring/adrci-usage.md","size":18421,"sha256":"b11acc1e8d1fafc5a9c52c3f81384847c9958ac0f55ddc072df64da54340fe3c","contentType":"text/markdown; charset=utf-8"},{"id":"b75a979b-a729-570d-94bc-82a4fd1e8072","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b75a979b-a729-570d-94bc-82a4fd1e8072/attachment.md","path":"monitoring/alert-log-analysis.md","size":17540,"sha256":"3f30e60fc9e09bf94a53d106ebc7d38796dc9fddcacf867a76646a34b9f2c6ba","contentType":"text/markdown; charset=utf-8"},{"id":"23c22f34-da98-5fe5-8cb2-58f9e147fe3d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/23c22f34-da98-5fe5-8cb2-58f9e147fe3d/attachment.md","path":"monitoring/health-monitor.md","size":20592,"sha256":"9f0aa3a924685d2d763922b900194656baa52638e1143a48c686f72df491d8bc","contentType":"text/markdown; charset=utf-8"},{"id":"0ec35e27-aa46-511b-82eb-8f9c467229e7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0ec35e27-aa46-511b-82eb-8f9c467229e7/attachment.md","path":"monitoring/space-management.md","size":22708,"sha256":"715e2780ebd6d4e6adaeccd46a0ad5279e6312673d2bd164f2ac0f45da235774","contentType":"text/markdown; charset=utf-8"},{"id":"3210469d-2481-5299-aa6c-d6c59a65a282","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3210469d-2481-5299-aa6c-d6c59a65a282/attachment.md","path":"monitoring/top-sql-queries.md","size":22771,"sha256":"9d461a272a489615a9a31556100ddb3034be8dfce0ec3c1c4ab270bc6d4ee64b","contentType":"text/markdown; charset=utf-8"},{"id":"1822167b-c370-58c0-a5c7-7d4df0909100","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1822167b-c370-58c0-a5c7-7d4df0909100/attachment.md","path":"ords/ords-architecture.md","size":13512,"sha256":"9f6a234b2ae12d7290043a0d045fcc95ed13c8819550d48d11564c0bac0817ea","contentType":"text/markdown; charset=utf-8"},{"id":"1530009a-d3fc-54c9-ac7a-f2018157da4d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1530009a-d3fc-54c9-ac7a-f2018157da4d/attachment.md","path":"ords/ords-authentication.md","size":20307,"sha256":"6b7ae04de77e5d9f44c091ed18c9002b3c45e38cdf8eb43ad71d0b0faab4e14f","contentType":"text/markdown; charset=utf-8"},{"id":"d26f5afb-1775-54e8-ac0a-30704d474d99","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d26f5afb-1775-54e8-ac0a-30704d474d99/attachment.md","path":"ords/ords-auto-rest.md","size":13465,"sha256":"5347f3bb49bf44c06f86c2445fcac88e3c7342afe676663895b6cb1acaf9e84e","contentType":"text/markdown; charset=utf-8"},{"id":"61356a3d-6160-525d-8399-de50782e40f0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/61356a3d-6160-525d-8399-de50782e40f0/attachment.md","path":"ords/ords-file-upload-download.md","size":17516,"sha256":"89054ee46fed4195bc0eae55f860167517148181e87f82053e428e2bd97f9e40","contentType":"text/markdown; charset=utf-8"},{"id":"ad42029f-68fc-58d4-af0f-4ba0ae6f20f0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad42029f-68fc-58d4-af0f-4ba0ae6f20f0/attachment.md","path":"ords/ords-installation.md","size":27106,"sha256":"c35d44a43cf3ff7df7fdec151e1a204f6c0b5557a5662c17d43149e15cbda76e","contentType":"text/markdown; charset=utf-8"},{"id":"262a14cb-ae67-5a5a-9f45-6294e880b8cc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/262a14cb-ae67-5a5a-9f45-6294e880b8cc/attachment.md","path":"ords/ords-metadata-catalog.md","size":14402,"sha256":"1071159718045d1ba944b735a721965290a804021b94433ed3716211c712818f","contentType":"text/markdown; charset=utf-8"},{"id":"da7e82fa-93e3-59b0-b7b4-8a5f071ee635","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da7e82fa-93e3-59b0-b7b4-8a5f071ee635/attachment.md","path":"ords/ords-monitoring.md","size":18507,"sha256":"d82f6ace0cc9a62dab9f6769591f438af7107f5a3914ab16ff1256076c09fa90","contentType":"text/markdown; charset=utf-8"},{"id":"0c412360-6372-581f-b043-f580f83f7a70","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0c412360-6372-581f-b043-f580f83f7a70/attachment.md","path":"ords/ords-pl-sql-gateway.md","size":19269,"sha256":"bffea77b6d88e3e207e24ea8bf0a3a1eecc40534b25fcaec39e3a7df7887d734","contentType":"text/markdown; charset=utf-8"},{"id":"40863212-771f-55e1-a69d-362e22af3543","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/40863212-771f-55e1-a69d-362e22af3543/attachment.md","path":"ords/ords-pre-authenticated-requests.md","size":7775,"sha256":"5d5e2f5fef3438522bd3a6f71e58c52abe7bfee7e12a3991afa991f68f1d5ea2","contentType":"text/markdown; charset=utf-8"},{"id":"12a05647-4e44-5021-bf39-a0ef7a9bf126","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/12a05647-4e44-5021-bf39-a0ef7a9bf126/attachment.md","path":"ords/ords-rest-api-design.md","size":19366,"sha256":"21508b724de26273a01b9c38de5a687baacf4165ad8fb24d1f2714758c7f3e64","contentType":"text/markdown; charset=utf-8"},{"id":"c937d499-7d55-562e-89be-731a6a521efe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c937d499-7d55-562e-89be-731a6a521efe/attachment.md","path":"ords/ords-security.md","size":17998,"sha256":"0d402f31e6997d027ccc671aa7f4c0123473566de3a2ca07c7b3016902364f02","contentType":"text/markdown; charset=utf-8"},{"id":"5bfe400c-8259-5111-b25e-5e965b129095","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5bfe400c-8259-5111-b25e-5e965b129095/attachment.md","path":"ords/ords-sessionless-transactions.md","size":8259,"sha256":"7e3024ad93fa793bc879fa47c002e73ad19732ac6afca4b7154fd3e12408ed7d","contentType":"text/markdown; charset=utf-8"},{"id":"be6a18b0-bbee-561f-affc-0491c8eee3b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/be6a18b0-bbee-561f-affc-0491c8eee3b6/attachment.md","path":"performance/ash-analysis.md","size":16281,"sha256":"9fcd9ccd0a7136a66da4e0a6908d84a1d542098988050ebd9d2203a065e1b7c7","contentType":"text/markdown; charset=utf-8"},{"id":"2a5d6742-8e9a-55c9-a36b-e35627725e93","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2a5d6742-8e9a-55c9-a36b-e35627725e93/attachment.md","path":"performance/awr-reports.md","size":14022,"sha256":"603ff476a4acfacdcea40288f2f5655a0dfb65675f656d364a3e65fd94101652","contentType":"text/markdown; charset=utf-8"},{"id":"fa43568a-c14e-55ae-a08c-e3ea09ca188a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fa43568a-c14e-55ae-a08c-e3ea09ca188a/attachment.md","path":"performance/explain-plan.md","size":20408,"sha256":"fbde0ee8a1c357dba01235c5fcabb66ac0be16ee242974212dd3788b51122029","contentType":"text/markdown; charset=utf-8"},{"id":"d3af3a93-36ce-54bc-a9c3-86a9519e1a98","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3af3a93-36ce-54bc-a9c3-86a9519e1a98/attachment.md","path":"performance/index-strategy.md","size":21336,"sha256":"8d49512fcadeeed933b9fb8877f426df81a97c8f6864b113e4acd38c18a45740","contentType":"text/markdown; charset=utf-8"},{"id":"dfffdf05-016c-5a83-9994-684825b4b1ba","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dfffdf05-016c-5a83-9994-684825b4b1ba/attachment.md","path":"performance/memory-tuning.md","size":19136,"sha256":"120bc8ec7d7b9cb67b472949852e9cd61d74096418f670f6daf59aa03cf42419","contentType":"text/markdown; charset=utf-8"},{"id":"0e484b36-6a06-5962-952f-8762e117507b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e484b36-6a06-5962-952f-8762e117507b/attachment.md","path":"performance/optimizer-stats.md","size":16609,"sha256":"1c6ab40cab81d7cc9f602dec3cd0c1d1bf1494eb085a3a2f3fbd6f85906f6b01","contentType":"text/markdown; charset=utf-8"},{"id":"ee52bd91-0531-54a1-ad4a-6563128ffca0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee52bd91-0531-54a1-ad4a-6563128ffca0/attachment.md","path":"performance/wait-events.md","size":19343,"sha256":"3bcb12f86ca0e71b528fcadab5b79e6b59e59e8aeef58bf20df7abfdae94cba8","contentType":"text/markdown; charset=utf-8"},{"id":"5678d46b-01a2-56ed-9b77-78d3bc906777","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5678d46b-01a2-56ed-9b77-78d3bc906777/attachment.md","path":"plsql/plsql-code-quality.md","size":15346,"sha256":"e20b129acb171c4ce26ffc9e1d4ecdebde8bf3f4261e864c6d12d6ccd2c35304","contentType":"text/markdown; charset=utf-8"},{"id":"4819c0d7-2543-5f93-9e4c-ef41115ce0d2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4819c0d7-2543-5f93-9e4c-ef41115ce0d2/attachment.md","path":"plsql/plsql-collections.md","size":14853,"sha256":"6e29e4318e1def6f7977b9371c43eb4de678a10b6187f64cb076bf0ae2e9da28","contentType":"text/markdown; charset=utf-8"},{"id":"4cf66b10-c1c3-5238-888b-f2671394a7ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4cf66b10-c1c3-5238-888b-f2671394a7ee/attachment.md","path":"plsql/plsql-compiler-options.md","size":15490,"sha256":"ee2e6b90803b4e591de79b76e890eb29c73b882b4fa77d3787761e58fb1c8684","contentType":"text/markdown; charset=utf-8"},{"id":"c258ebd2-3268-510b-9b7e-8ef16a3a11f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c258ebd2-3268-510b-9b7e-8ef16a3a11f2/attachment.md","path":"plsql/plsql-cursors.md","size":15478,"sha256":"10fab5a5df06d28e184d8a1545c9e3566f4dd3487f75d09271039f9feb606ab8","contentType":"text/markdown; charset=utf-8"},{"id":"45a08c02-4dca-5a48-8873-fd0216f27a12","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/45a08c02-4dca-5a48-8873-fd0216f27a12/attachment.md","path":"plsql/plsql-debugging.md","size":16120,"sha256":"bd987f85e315e01d9337481a834778b3e869d8b35341405892084b8d0f86bd14","contentType":"text/markdown; charset=utf-8"},{"id":"ad582288-cab0-5618-8cca-ae9fee03d71e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad582288-cab0-5618-8cca-ae9fee03d71e/attachment.md","path":"plsql/plsql-error-handling.md","size":17426,"sha256":"5a910c3e34bf4a195abb4209299b5269b2b474cc55bc591a74d2cfb2c75db59c","contentType":"text/markdown; charset=utf-8"},{"id":"3f4906ba-51e1-5ea5-a497-47ded9e4d252","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3f4906ba-51e1-5ea5-a497-47ded9e4d252/attachment.md","path":"plsql/plsql-package-design.md","size":14193,"sha256":"5698893d477aa11f14cbb78a6df19a336d5477b8c0a8063d406a2c33d336ab97","contentType":"text/markdown; charset=utf-8"},{"id":"ad8f9ea4-93fe-5aa7-9f4f-834981dde23e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad8f9ea4-93fe-5aa7-9f4f-834981dde23e/attachment.md","path":"plsql/plsql-patterns.md","size":19400,"sha256":"0cd96d4107ff998a306096491c6bc1088a921255fbe7587e11948f6792587084","contentType":"text/markdown; charset=utf-8"},{"id":"7b032b92-aad0-526c-801f-304bb6c2ccce","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7b032b92-aad0-526c-801f-304bb6c2ccce/attachment.md","path":"plsql/plsql-performance.md","size":16316,"sha256":"8fa6fee0b0efa2d14baf616390fdfb44c34ca8b19501f2dd603df96af7cdef18","contentType":"text/markdown; charset=utf-8"},{"id":"4e4a03c2-4b19-5e84-a978-a4c533b6cf30","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4e4a03c2-4b19-5e84-a978-a4c533b6cf30/attachment.md","path":"plsql/plsql-security.md","size":15032,"sha256":"ebfec3a3c7d27633bb6df862bc3f8525d882b41dc9d14dde9a4248399cd077a9","contentType":"text/markdown; charset=utf-8"},{"id":"022c80f0-0fd3-5ae3-9c3f-9b37351e2483","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/022c80f0-0fd3-5ae3-9c3f-9b37351e2483/attachment.md","path":"security/auditing.md","size":21158,"sha256":"414966963746d230b459315b466b5d52e8aa65b5e381ecf745685ccfac10b679","contentType":"text/markdown; charset=utf-8"},{"id":"28a1fff1-9903-5208-8d6a-97693defaadd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28a1fff1-9903-5208-8d6a-97693defaadd/attachment.md","path":"security/data-masking.md","size":20705,"sha256":"dc4c021779ece1736e875bf6435a3db3f6b7db69ecb4379a46b7d3e860b9ca04","contentType":"text/markdown; charset=utf-8"},{"id":"f1825964-cc4c-56c3-a122-f1d80f4a98df","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f1825964-cc4c-56c3-a122-f1d80f4a98df/attachment.md","path":"security/encryption.md","size":20997,"sha256":"418389713f80013e7590bb9036cb08bd1118d28a0016c51adc6b96cf3177b42c","contentType":"text/markdown; charset=utf-8"},{"id":"2e2c186c-1ea9-5a40-9a72-5ae16afa3c83","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2e2c186c-1ea9-5a40-9a72-5ae16afa3c83/attachment.md","path":"security/network-security.md","size":22594,"sha256":"69bb184e7dbe8b3136080706b169745bdc910eb2c262a13db0bab03c4e458377","contentType":"text/markdown; charset=utf-8"},{"id":"0cb9cf8a-69a7-5d87-a28c-b47543fe16d5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0cb9cf8a-69a7-5d87-a28c-b47543fe16d5/attachment.md","path":"security/privilege-management.md","size":19561,"sha256":"ce248584aac584a301e93118a2055c37c66fdf875916b82fdc1a894c896a1f1d","contentType":"text/markdown; charset=utf-8"},{"id":"f4776c45-8902-55b3-8636-bee951a64184","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f4776c45-8902-55b3-8636-bee951a64184/attachment.md","path":"security/row-level-security.md","size":20953,"sha256":"6af1854e009546e85910790e8b8a887c9ed4f0e104b04cec31d809ed9b34469b","contentType":"text/markdown; charset=utf-8"},{"id":"04fa79a2-c162-5a22-8c96-6b12f9fd95f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/04fa79a2-c162-5a22-8c96-6b12f9fd95f5/attachment.md","path":"sql-dev/dynamic-sql.md","size":22668,"sha256":"7a0b2e734a809f98f594374b42f8ffca5ed9fa225d8b2f055ef6cc8084ba0082","contentType":"text/markdown; charset=utf-8"},{"id":"2188717c-bf02-5cc9-806c-820a5a6ec91f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2188717c-bf02-5cc9-806c-820a5a6ec91f/attachment.md","path":"sql-dev/pl-sql-best-practices.md","size":19973,"sha256":"903b03e3658dfffea199636ee4be1449136cbe5d5380385980255a25f8d3488f","contentType":"text/markdown; charset=utf-8"},{"id":"e23f7441-ae24-512f-b605-312fe751cd19","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e23f7441-ae24-512f-b605-312fe751cd19/attachment.md","path":"sql-dev/sql-best-practices.md","size":48783,"sha256":"05a875b055a38a5c3327f1b8b0c1425e5bc67f4ad86d2f284449649448ff709c","contentType":"text/markdown; charset=utf-8"},{"id":"9ec9ba48-66c7-5070-bf92-789465c5c64b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9ec9ba48-66c7-5070-bf92-789465c5c64b/attachment.md","path":"sql-dev/sql-injection-avoidance.md","size":16883,"sha256":"56ca03a4297b3936543f63314205bb361dc30bb74e58e590363fbfa2cf11d19b","contentType":"text/markdown; charset=utf-8"},{"id":"804995db-7dcc-5e04-a3fa-85682db43d6d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/804995db-7dcc-5e04-a3fa-85682db43d6d/attachment.md","path":"sql-dev/sql-patterns.md","size":19352,"sha256":"973b168fbd13848d2b5989961994e55f822721cb745afb991c5fc014b84813c7","contentType":"text/markdown; charset=utf-8"},{"id":"2dc51323-7bd1-5a74-9c13-67111069415a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2dc51323-7bd1-5a74-9c13-67111069415a/attachment.md","path":"sql-dev/sql-tuning.md","size":15403,"sha256":"51d0f2df664bc1a4dd5132b817dd6e666d76d61e98a56fa302e7020047ac1380","contentType":"text/markdown; charset=utf-8"},{"id":"920b4f3f-8af8-5343-ad5b-0bb709cd8b0a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/920b4f3f-8af8-5343-ad5b-0bb709cd8b0a/attachment.md","path":"sqlcl/sqlcl-awr.md","size":7144,"sha256":"3a37a1115e81f6f59a2acf3c9ada367263040d2820ce735c9a66f1f54ec211aa","contentType":"text/markdown; charset=utf-8"},{"id":"22d1e54a-9ede-5741-9c1e-47ae810a19be","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/22d1e54a-9ede-5741-9c1e-47ae810a19be/attachment.md","path":"sqlcl/sqlcl-background-jobs.md","size":6814,"sha256":"b06e095f4ab47978e7e18ef6bca2c8dd92465f6f2dff7c54b93322375c20c3e1","contentType":"text/markdown; charset=utf-8"},{"id":"1f5629b5-b3d7-57cb-957d-f5e19664e8d1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1f5629b5-b3d7-57cb-957d-f5e19664e8d1/attachment.md","path":"sqlcl/sqlcl-basics.md","size":15697,"sha256":"7b1a3b2b8b9ea5113f58f6132ba6e2a0854d84549982a0b1f04e2449c073f10c","contentType":"text/markdown; charset=utf-8"},{"id":"5361355c-f30e-5bc9-a451-96d7e68f1ca1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5361355c-f30e-5bc9-a451-96d7e68f1ca1/attachment.md","path":"sqlcl/sqlcl-cicd.md","size":19674,"sha256":"9570e326bf42b3412b0048efff415c22820cd95fc05f5a59531097fd1f800724","contentType":"text/markdown; charset=utf-8"},{"id":"d3180c88-a4a3-545e-8dda-df694aad34ff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3180c88-a4a3-545e-8dda-df694aad34ff/attachment.md","path":"sqlcl/sqlcl-data-loading.md","size":15047,"sha256":"2e69fb3bb76a24cf13719343efce4270829bc71344d8edb27c6e17e8912a4851","contentType":"text/markdown; charset=utf-8"},{"id":"50d8a1d0-b68c-5760-bfc3-22aac40724e0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/50d8a1d0-b68c-5760-bfc3-22aac40724e0/attachment.md","path":"sqlcl/sqlcl-ddl-generation.md","size":16799,"sha256":"e7cf526e52829473d5562efbce7aebf0b167149ce6031fcaa110e61b44f66681","contentType":"text/markdown; charset=utf-8"},{"id":"dd187a5f-0903-5a21-a107-b3f637a1cc05","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dd187a5f-0903-5a21-a107-b3f637a1cc05/attachment.md","path":"sqlcl/sqlcl-formatting.md","size":15863,"sha256":"ce67ef64fa04d9ee0c7ed1775c753db985cee462be1a6f5f516e459d98523584","contentType":"text/markdown; charset=utf-8"},{"id":"dc860a4d-7a0e-5fc9-9124-2ffaee38957c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc860a4d-7a0e-5fc9-9124-2ffaee38957c/attachment.md","path":"sqlcl/sqlcl-liquibase.md","size":15502,"sha256":"37f132d8eecd99ffaf7ceec69d0474ce3d04ee94cc21312381eb94010b1959a9","contentType":"text/markdown; charset=utf-8"},{"id":"30ceb643-a2ac-5502-8bb4-c4c76b79a895","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/30ceb643-a2ac-5502-8bb4-c4c76b79a895/attachment.md","path":"sqlcl/sqlcl-mcp-server.md","size":17852,"sha256":"85c6a4865e13a78b702990a78e4aef38cbc585b297cedd25fe0579fb8a30c2ab","contentType":"text/markdown; charset=utf-8"},{"id":"42768c1d-8ac6-5135-aaf1-cbd30d91d8e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/42768c1d-8ac6-5135-aaf1-cbd30d91d8e3/attachment.md","path":"sqlcl/sqlcl-scheduler-daemon.md","size":10635,"sha256":"e468a08ac4306d4681b553892d65f576bdc5a0406e20b06da96c94f0f483a835","contentType":"text/markdown; charset=utf-8"},{"id":"575282a5-21e3-5c55-b714-3d132c96c390","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/575282a5-21e3-5c55-b714-3d132c96c390/attachment.md","path":"sqlcl/sqlcl-scripting.md","size":15547,"sha256":"602363a3e85ef0770ff3b893e49878e64ee327e8abf5797c71aa62dff4bc441b","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"be380af1dafe312ec2564998ace9ca60fd5d1f17a0be28b2c2fdd1dce859b42f","attachment_count":155,"text_attachments":155,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":2,"skill_md_path":"db/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":1},"version":"v1","category":"security","import_tag":"clean-skills-v1","description":"Oracle Database guidance for SQL, PL/SQL, SQLcl, ORDS, administration, app development, performance, security, migrations, and agent-safe database workflows. Use when the user asks to write, edit, rewrite, review, format, debug, tune, or explain SQL; create or refactor PL/SQL; use SQLcl, Liquibase, ORDS, JDBC, node-oracledb, Python, Java, .NET, or database frameworks; troubleshoot queries, sessions, locks, waits, indexes, optimizer plans, AWR, ASH, migrations, schemas, users, roles, privileges, backup, recovery, Data Guard, RAC, multitenant, containers, monitoring, auditing, encryption, VPD, or safe agent database operations."}},"renderedAt":1782986517493}

Oracle Database Skills This domain contains Oracle Database skills for administration, SQL and PL/SQL development, performance tuning, security, ORDS, SQLcl, migrations, frameworks, OCR container guidance, and agent-safe database workflows. How to Use This Domain 1. Start with the routing table below. 2. Read only the specific file or category you need. Directory Structure Category Routing | Topic | Directory | |-------|-----------| | Backup, recovery, RMAN, Data Guard, redo/undo logs, users | | | Safe DML, destructive operation guards, idempotency, schema discovery, ORA- error handling | | |…