Oracle
This guide walks you through connecting your Oracle database to Solid. By the end, Solid will be able to discover your data, collect metadata, and analyze query history.
Before You Begin
Make sure you have the following:
- Admin access to the Oracle Pluggable Database (PDB) you want to connect
- A SQL client (e.g., SQL*Plus, SQL Developer, or similar)
- The PDB service name — you'll need this for the Solid connection string
What is a PDB?
Oracle databases use a "container" architecture. The top level is the CDB (Container Database), and inside it are one or more PDBs (Pluggable Databases). Solid connects at the PDB level — this is where your actual data lives.
Step 1 — Create the Solid User
Run this script while connected as an admin user inside the target PDB.
-- Set your PDB as the active container
ALTER SESSION SET CONTAINER = {db_name};
-- Create the Solid user
CREATE USER solid_read_only_user IDENTIFIED BY "{password}";
-- Allow the user to log in
GRANT CREATE SESSION TO solid_read_only_user;
-- Allow access to database structure and metadata
GRANT SELECT ANY DICTIONARY TO solid_read_only_user;
-- Allow read-only access to table data
GRANT SELECT ANY TABLE TO solid_read_only_user;
-- Lock the user to this PDB only (prevents cross-container access)
ALTER USER solid_read_only_user SET CONTAINER_DATA = CURRENT;Replace {db_name} with your PDB name and {password} with a strong password.
Repeat for each PDB you want to connect to Solid.
Step 2 — Verify the Setup
Log in as solid_read_only_user and run these checks to confirm everything is working.
Check 1 — User is local to the PDB
SELECT username, common, oracle_maintained
FROM dba_users
WHERE username = 'SOLID_READ_ONLY_USER';Expected: COMMON = NO
Check 2 — User cannot access the root container
ALTER SESSION SET CONTAINER = CDB$ROOT;Expected: ORA-01031: insufficient privileges ← this error means isolation is working correctly
Check 3 — User can read metadata
SELECT owner, table_name
FROM dba_tables
WHERE owner = '{schema}'
AND ROWNUM <= 5;Expected: Rows returned without errors
Check 4 — User can read table data
SELECT *
FROM {schema}.{table}
WHERE ROWNUM <= 1;Expected: A row returned without errors
Step 3 — Connect to Solid
In Solid, create a new Oracle data source connection using:
| Field | Value |
|---|---|
| Host | Your Oracle listener address |
| Port | 1521 (default) |
| Service name | Your PDB service name |
| Username | solid_read_only_user |
| Password | The password you set above |
Use the PDB service name, not the CDB SID. Using the CDB SID may result in connection or permission errors.
What Solid Collects
Here's a plain-English summary of what Solid reads from your database — and what it never touches.
Metadata (always collected)
Structure of your database — no row data involved.
| What | Why |
|---|---|
| Table and column names, types, and properties | To build the data catalog |
| Primary and foreign key relationships | To map data lineage |
| Schema and object ownership | To organize by team and domain |
| Table creation and modification timestamps | To track data freshness |
| Table and column comments | To surface business descriptions |
Query History (always collected)
Used to understand how data is actually being used.
| What | Why |
|---|---|
| SQL query text (SELECT only) | To learn which tables and columns are popular |
| Executing user and role | To understand who uses what |
| Execution timestamps and duration | To identify performance patterns |
Query history requires AWR.
Solid reads from Oracle's Automatic Workload Repository (AWR), which requires the Oracle Diagnostics Pack license (included with Enterprise Edition). Check with your DBA if you're unsure whether AWR is enabled.
User Information (always collected)
| What | Why |
|---|---|
| Usernames, IDs, account status | To attribute query history to users |
| Last login date | To identify active vs. inactive users |
Table Usage Statistics (always collected)
Daily counts of inserts, updates, and deletes per table — used to surface active tables and detect stale data.
Data Profiling (optional)
Only collected if SELECT ANY TABLE is granted. Solid samples a small number of rows to compute:
- MIN / MAX / AVG for numeric columns
- Distinct value counts for text columns
- Date ranges for date columns
- Null rates and basic data quality metrics
Troubleshooting
"Insufficient privileges" errors
Symptoms: Errors when Solid tries to read metadata or table structure.
- Confirm the grants were run inside the PDB, not at the CDB level
- Verify the user's privileges:
SELECT * FROM dba_sys_privs
WHERE grantee = 'SOLID_READ_ONLY_USER';You should see rows for CREATE SESSION, SELECT ANY DICTIONARY, and SELECT ANY TABLE.
Query history is missing or empty
Symptoms: Solid shows no query history after connecting.
- Confirm AWR is collecting snapshots:
SELECT MIN(end_interval_time), MAX(end_interval_time)
FROM dba_hist_snapshot;If this returns no rows, AWR is not running. Contact your DBA to enable it.
- Check that Solid's
last_collecttimestamp falls within AWR's retention window (typically 8–30 days).
Can't connect to the database
Symptoms: Solid returns a connection timeout or authentication error.
- Confirm you're using the PDB service name, not the CDB SID
- Check the account isn't locked:
SELECT username, account_status, lock_date
FROM dba_users
WHERE username = 'SOLID_READ_ONLY_USER';- Verify the Oracle listener is reachable on port
1521from Solid's network
Security Notes
The solid_read_only_user account is built on least-privilege principles:
- ✔ Read-only — no INSERT, UPDATE, DELETE, or DDL privileges
- ✔ PDB-scoped — cannot access the root CDB or other PDBs
- ✔ No admin roles —
DBA,SYSDBA, and similar roles are not granted
Additional recommendations:
- Use a strong, unique password and rotate it per your organization's policy
- Lock the account when not in use for extended periods
- Restrict access to Oracle's listener port (
1521) via firewall rules - Enable SSL/TLS or Oracle Wallet for encrypted connections where available
Multi-Database Support
Solid currently supports one PDB per connection. To monitor multiple databases, add a separate Solid data source for each PDB.
Support for discovering multiple databases through a single connection is planned for a future release.
Supported Oracle Versions
| Version | Notes |
|---|---|
| Oracle 12c (12.2+) | Supported — PDB architecture required |
| Oracle 18c | Supported |
| Oracle 19c | Supported (recommended LTS version) |
| Oracle 21c | Supported |
| Oracle 23c | Supported |
Non-CDB Oracle deployments are not supported. If you're running a traditional single-instance Oracle database without PDBs, contact your Solid administrator.
Appendix — Collect Queries
These are the SQL queries Solid runs internally to collect metadata and usage data. They are provided for transparency and for DBAs who want to audit exactly what Solid reads.
Columns
SELECT
(SELECT NAME FROM V$PDBS) AS TABLE_CATALOG,
atc.OWNER AS TABLE_SCHEMA,
atc.TABLE_NAME,
atc.COLUMN_NAME,
atc.COLUMN_ID AS ORDINAL_POSITION,
atc.DATA_TYPE,
CASE WHEN atc.NULLABLE = 'Y' THEN 'YES' ELSE 'NO' END AS IS_NULLABLE,
atc.DATA_DEFAULT AS COLUMN_DEFAULT,
tc.COMMENTS AS TABLE_COMMENT,
cc.COMMENTS AS COLUMN_COMMENT,
ao.CREATED AS TABLE_CREATED_AT,
ao.CREATED AS COLUMN_CREATED_AT,
COALESCE(atm.TIMESTAMP, ao.CREATED) AS TABLE_UPDATED_AT,
NVL(cons.IS_PK, 'FALSE') AS IS_PK,
NVL(cons.IS_FK, 'FALSE') AS IS_FK,
CASE
WHEN idx.COLUMN_NAME IS NOT NULL THEN 'TRUE'
ELSE 'FALSE'
END AS IS_INDEX
FROM DBA_TAB_COLUMNS atc
JOIN DBA_TABLES at ON atc.OWNER = at.OWNER AND atc.TABLE_NAME = at.TABLE_NAME
JOIN DBA_OBJECTS ao ON atc.TABLE_NAME = ao.OBJECT_NAME AND atc.OWNER = ao.OWNER
LEFT JOIN DBA_TAB_MODIFICATIONS atm ON atc.OWNER = atm.TABLE_OWNER AND atc.TABLE_NAME = atm.TABLE_NAME
LEFT JOIN DBA_TAB_COMMENTS tc ON atc.OWNER = tc.OWNER AND atc.TABLE_NAME = tc.TABLE_NAME
LEFT JOIN DBA_COL_COMMENTS cc ON atc.OWNER = cc.OWNER AND atc.TABLE_NAME = cc.TABLE_NAME AND atc.COLUMN_NAME = cc.COLUMN_NAME
LEFT JOIN (
SELECT DISTINCT TABLE_OWNER, TABLE_NAME, COLUMN_NAME
FROM DBA_IND_COLUMNS
) idx ON atc.OWNER = idx.TABLE_OWNER
AND atc.TABLE_NAME = idx.TABLE_NAME
AND atc.COLUMN_NAME = idx.COLUMN_NAME
LEFT JOIN (
SELECT
acc.OWNER,
acc.TABLE_NAME,
acc.COLUMN_NAME,
MAX(CASE WHEN ac.CONSTRAINT_TYPE = 'P' THEN 'TRUE' ELSE 'FALSE' END) AS IS_PK,
MAX(CASE WHEN ac.CONSTRAINT_TYPE = 'R' THEN 'TRUE' ELSE 'FALSE' END) AS IS_FK
FROM DBA_CONSTRAINTS ac
JOIN DBA_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME AND ac.OWNER = acc.OWNER
WHERE ac.CONSTRAINT_TYPE IN ('P', 'R')
GROUP BY acc.OWNER, acc.TABLE_NAME, acc.COLUMN_NAME
) cons
ON atc.OWNER = cons.OWNER
AND atc.TABLE_NAME = cons.TABLE_NAME
AND atc.COLUMN_NAME = cons.COLUMN_NAME
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
AND atc.owner IN ({schema_list})
ORDER BY atc.OWNER, atc.TABLE_NAME, atc.COLUMN_ID;Foreign Keys and Primary Keys
SELECT
(SELECT NAME FROM V$PDBS) AS fk_database_name,
acc.OWNER AS fk_schema_name,
acc.TABLE_NAME AS fk_table_name,
acc.COLUMN_NAME AS fk_column_name,
(SELECT NAME FROM V$PDBS) AS pk_database_name,
p_acc.OWNER AS pk_schema_name,
p_acc.TABLE_NAME AS pk_table_name,
p_acc.COLUMN_NAME AS pk_column_name,
ac.R_CONSTRAINT_NAME AS pk_name,
acc.POSITION AS key_sequence,
'NO ACTION' AS update_rule,
ac.delete_rule,
ac.CONSTRAINT_NAME AS fk_name,
ac.deferrable,
ac.deferred,
ac.rely,
ao.CREATED AS created_on
FROM DBA_CONS_COLUMNS acc
JOIN DBA_CONSTRAINTS ac
ON acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND acc.OWNER = ac.OWNER
JOIN DBA_OBJECTS ao
ON acc.OWNER = ao.OWNER AND acc.TABLE_NAME = ao.OBJECT_NAME
JOIN DBA_CONS_COLUMNS p_acc
ON ac.R_CONSTRAINT_NAME = p_acc.CONSTRAINT_NAME
AND ac.R_OWNER = p_acc.OWNER
AND acc.POSITION = p_acc.POSITION
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
AND acc.OWNER IN ({schema_list})
AND ac.CONSTRAINT_TYPE = 'R'
AND ao.OBJECT_TYPE = 'TABLE'
AND acc.TABLE_NAME NOT LIKE 'BIN$%'
ORDER BY acc.OWNER, acc.TABLE_NAME, ac.CONSTRAINT_NAME, acc.POSITION;Query History
WITH USER_ROLES_CTE AS (
SELECT
GRANTEE,
LISTAGG(DISTINCT GRANTED_ROLE, ', ') WITHIN GROUP (ORDER BY GRANTED_ROLE) AS ASSIGNED_ROLES
FROM DBA_ROLE_PRIVS
GROUP BY GRANTEE
)
SELECT
stat.SQL_ID AS QUERY_ID,
q.SQL_TEXT AS QUERY_TEXT,
p.NAME AS DATABASE_NAME,
stat.PARSING_SCHEMA_NAME AS SCHEMA_NAME,
CASE q.COMMAND_TYPE
WHEN 3 THEN 'SELECT'
WHEN 2 THEN 'INSERT'
WHEN 6 THEN 'UPDATE'
WHEN 7 THEN 'DELETE'
WHEN 189 THEN 'MERGE'
ELSE 'OTHER (' || q.COMMAND_TYPE || ')'
END AS QUERY_TYPE,
u.USERNAME AS USER_NAME,
r.ASSIGNED_ROLES AS ROLE_NAME,
CASE
WHEN NVL(stat.ROWS_PROCESSED_DELTA, 0) > 0 THEN 'SUCCESS'
ELSE 'FAILED'
END AS EXECUTION_STATUS,
snaps.END_INTERVAL_TIME - NUMTODSINTERVAL(stat.ELAPSED_TIME_DELTA / 1000000, 'SECOND') AS START_TIME,
snaps.END_INTERVAL_TIME AS END_TIME,
ROUND(NVL(stat.ELAPSED_TIME_DELTA, 0) / 1000, 2) AS TOTAL_ELAPSED_TIME_MS,
u.ORACLE_MAINTAINED AS IS_ORACLE_MAINTAINED
FROM DBA_HIST_SQLSTAT stat
JOIN DBA_HIST_SNAPSHOT snaps ON stat.SNAP_ID = snaps.SNAP_ID AND stat.DBID = snaps.DBID
JOIN DBA_HIST_SQLTEXT q ON stat.SQL_ID = q.SQL_ID AND stat.DBID = q.DBID AND stat.CON_DBID = q.CON_DBID
JOIN DBA_USERS u ON stat.PARSING_USER_ID = u.USER_ID
JOIN V$PDBS p ON stat.CON_ID = p.CON_ID
LEFT JOIN USER_ROLES_CTE r ON u.USERNAME = r.GRANTEE
WHERE p.NAME = '{db_name}'
AND (u.ORACLE_MAINTAINED = 'N' OR u.USERNAME != 'SYS')
AND snaps.END_INTERVAL_TIME > TIMESTAMP '{last_collect}'
AND q.COMMAND_TYPE = 3
ORDER BY snaps.END_INTERVAL_TIME DESC, stat.SQL_ID
FETCH FIRST 1000000 ROWS ONLY;Users
SELECT
user_id,
username AS name,
created AS created_on,
'' AS deleted_on,
CASE
WHEN account_status LIKE '%LOCKED%' THEN 'LOCKED'
WHEN account_status = 'OPEN' THEN 'ACTIVE'
ELSE account_status
END AS status,
username AS login_name,
username AS display_name,
last_login,
'' AS first_name,
'' AS last_name,
'' AS email,
CASE WHEN oracle_maintained = 'N' THEN 'HUMAN' ELSE 'MACHINE' END AS user_type
FROM dba_users
ORDER BY username;Table Usage Statistics
SELECT
(SELECT NAME FROM V$PDBS) AS DATABASE_NAME,
at.OWNER AS SCHEMA_NAME,
at.TABLE_NAME AS TABLE_NAME,
TO_CHAR(TRUNC(atm.TIMESTAMP), 'YYYY-MM-DD') AS DAY,
SUM(atm.INSERTS) AS TOTAL_ROWS_ADDED,
SUM(atm.DELETES) AS TOTAL_ROWS_REMOVED,
SUM(atm.UPDATES) AS TOTAL_ROWS_UPDATED,
(SUM(atm.INSERTS) + SUM(atm.DELETES) + SUM(atm.UPDATES)) AS TOTAL_COUNT,
MAX(at.LAST_ANALYZED) AS LAST_ANALYZED
FROM DBA_TABLES at
JOIN DBA_TAB_MODIFICATIONS atm
ON atm.TABLE_OWNER = at.OWNER AND atm.TABLE_NAME = at.TABLE_NAME
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
AND at.OWNER IN ({schema_list})
AND atm.TIMESTAMP BETWEEN TIMESTAMP '{start_day_to_collect}' AND TIMESTAMP '{end_day_to_collect}'
GROUP BY at.OWNER, at.TABLE_NAME, TRUNC(atm.TIMESTAMP)
HAVING (SUM(atm.INSERTS) + SUM(atm.DELETES) + SUM(atm.UPDATES)) > 0
ORDER BY DAY, SCHEMA_NAME, TABLE_NAME;Updated 7 days ago
