Tales From A Lazy Fat DBA

Its all about Databases, their performance, troubleshooting & much more …. ¯\_(ツ)_/¯

Archive for August, 2026

Defragmenting Very Large MySQL Tables Without Losing Your Weekend: Why I Chose pt-online-schema-change

Posted by FatDBA on August 29, 2026

Large MySQL table defragmentation looks easy on paper. You see a 500 GB or 1 TB .ibd file, notice that the table has gone through years of INSERTs, UPDATEs and DELETEs, and the obvious thought is: rebuild it and get the space back. That was exactly where I started …

What I learned very quickly is that once a table reaches a few hundred gigabytes, defragmentation stops being a simple maintenance command and becomes an operational engineering exercise. You have to think about stale statistics, how InnoDB actually uses pages, temporary space requirements, metadata locks, binary log generation, replica lag, recovery from interruption and, perhaps most importantly, whether the rebuild will reclaim any meaningful space at all. During my testing, I used native MySQL rebuild methods, looked closely at gh-ost, and eventually standardized on Percona Toolkit’s pt-online-schema-change. But the real solution was not just choosing pt-osc. The bigger win came from building a restartable and monitorable automation layer around it.

The examples below use synthetic table and schema names, and some sizes are rounded, but the behaviour and lessons come directly from real large-table testing.

The first surprise: a large .ibd does not automatically mean a large reclaim opportunity.

-- One of the first discovery queries I used was fairly standard:
SET SESSION information_schema_stats_expiry = 0;
SELECT
    table_schema,
    table_name,
    table_rows,
    ROUND(data_length/1024/1024/1024,2)  AS data_gib,
    ROUND(index_length/1024/1024/1024,2) AS index_gib,
    ROUND(data_free/1024/1024/1024,2)    AS data_free_gib,
    ROUND((data_length+index_length)/1024/1024/1024,2) AS used_gib
FROM information_schema.tables
WHERE table_schema='LAB'
  AND table_name='position_history_2023';

-- At the OS level I checked the actual tablespace:
ls -lh /var/lib/mysql/LAB/position_history_2023.ibd

-- One table gave me something that initially looked fantastic:
Physical .ibd        : 356.64 GiB
DATA_LENGTH          : 19.22 GiB
INDEX_LENGTH         : 32.44 GiB
Reported used        : 51.66 GiB
DATA_FREE            : 0.01 GiB

-- The first calculation looked obvious:
356.64 - 51.66 = ~305 GiB potential reclaim

-- If I had trusted that number, I would have scheduled a long rebuild immediately. Instead, I checked the InnoDB persistent statistics:

SELECT
    database_name,
    table_name,
    last_update,
    n_rows,
    clustered_index_size,
    sum_of_other_index_sizes
FROM mysql.innodb_table_stats
WHERE database_name='LAB'
  AND table_name='position_history_2023';

-- The statistics were years old and MySQL estimated only around 42 million rows. I refreshed them:
ANALYZE TABLE LAB.position_history_2023;

-- Then ran the size query again. This time the result looked completely different:
Estimated rows       : ~297 million
DATA                 : 134.55 GiB
INDEX                : 217.89 GiB
Used                 : 352.44 GiB
Physical .ibd        : 356.64 GiB
Difference           : ~4.20 GiB

-- The apparent 305 GiB reclaim opportunity had disappeared simply because the statistics were refreshed.

So, the first lesson learned for anyone … Never call physical .ibd - DATA_LENGTH - INDEX_LENGTH “reclaimable space” unless you first know the statistics are current. At best, treat it as a signal.

Now, Why I became uncomfortable with native MySQL rebuilds ??

The most obvious native approach is something like:

OPTIMIZE TABLE LAB.big_table; OR ... ALTER TABLE LAB.big_table ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;

For small tables, or where a maintenance window is available, I still use native methods. There is nothing inherently wrong with them. The problem appears when the table is hundreds of gigabytes or larger. …. The OPTIMIZE TABLE for InnoDB is essentially mapped to an ALTER TABLE operation that rebuilds the table and updates index statistics. MySQL also notes that secondary indexes may not be created as efficiently during this process because the secondary-key entries are inserted according to primary-key order.

That immediately leads to one assumption that I had to unlearn —> A rebuild does not guarantee that the .ibd file will become smaller.

The command itself was perfectly valid: ALTER TABLE LAB.native_test ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;

Before and after I checked: ls -lh /var/lib/mysql/LAB/native_test.ibd … The test result was essentially:
Before rebuild : X GiB
After rebuild : X + several GiB

The exact number was less important than the behaviour. Nothing had failed. InnoDB had simply produced a different page and index layout during reconstruction. Secondary index layout, page occupancy and record distribution all influence the final physical size. That is why I no longer equate the word rebuild with the word shrink. The other thing that bothered me was the word ONLINE. Many people read –> LOCK=NONE and mentally translate it into: low-risk background operation .. That is not what MySQL promises.

For large online DDL operations that rebuild tables, MySQL explicitly documents several limitations: there is no native pause mechanism, no native CPU or I/O throttling mechanism, rollback can be expensive if the operation fails, and long-running online DDL can create replication lag. The operation also still needs an exclusive metadata lock during its final phase. That matters a lot when your table is 1 TB.

Imagine this: ALTER TABLE LAB.one_terabyte_table ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;

It starts at 8:00 AM and everything looks fine. Six hours later application workload increases sharply. Now I want to say “Pause the rebuild for one hour” .. Sorry, there is no simple native control for that. Or maybe the filesystem begins filling faster than expected and I want to reduce the copy rate. Again, there is no convenient built-in throttle control for the running operation. And if that large operation fails after many hours, rollback itself can be expensive. MySQL specifically calls that out as one of the limitations of large online DDL operations.

Disk space is another concern. Online DDL that rebuilds indexes can need substantial temporary space, and the operation can fail if tmpdir or innodb_tmpdir runs out of space. Concurrent DML is also buffered in the online DDL log, and if that log becomes too large relative to innodb_online_alter_log_max_size, MySQL can fail the operation with an online-log-too-big condition.

Then comes the final cutover. Even an online operation must eventually update the table definition. MySQL therefore needs an exclusive metadata lock during that final phase. A long-running or forgotten transaction holding a metadata lock can block completion. For a 15-minute rebuild, I can live with some of that. For a 20-hour rebuild, I want more control.

Where gh-ost fits, and why I still did not choose it here ??
I spent time looking at gh-ost because technically it has a very attractive architecture. Unlike pt-online-schema-change, gh-ost is designed around a triggerless migration model. It reads changes through the binary log instead of creating three synchronization triggers on the original table. That is a significant design advantage in some environments.

The gh-ost project explains that trigger-based migration tools add synchronous work to application DML, and that even if copying is throttled, the triggers themselves must continue running because removing them would break synchronization. That is a fair criticism of trigger-based online schema change.

gh-ost also gives excellent throttling controls. It can throttle based on replication lag, server load, flag files, control replicas and custom queries. Its cutover design is also carefully engineered around an atomic swap so clients either see the original or the new table rather than an intermediate period where neither table is available.

So why did I not use gh-ost? Because choosing an online schema-change tool should not become a religious argument. gh-ost has its own requirements and limitations. It depends heavily on binary-log behaviour, expects a suitable primary or unique key, and has limitations around foreign keys and triggers. For an environment already standardized on row-based replication and gh-ost operational patterns, it may absolutely be the better option. In my case, pt-online-schema-change fitted the existing DBA workflow better, and more importantly I could build the restart, recovery, validation and monitoring model I wanted around it. That became the deciding factor.

Why pt-online-schema-change finally won ?

pt-online-schema-change works on a shadow copy of the table. It creates the new table, modifies it, copies the rows in chunks, and keeps the new table synchronized with ongoing DML through triggers on the original table. Once copying completes, the original table is moved aside and the new one takes its place. The trigger approach does add overhead, and I do not pretend otherwise. What pt-osc gave me, however, was very good control of the actual copy process. For example, my wrapper uses settings such as:

–chunk-index=PRIMARY
–chunk-size=500
–chunk-time=0.5
–chunk-size-limit=4

–chunk-time allows pt-osc to dynamically adjust chunk size so copy queries target approximately the configured execution time. I also use explicit load thresholds mentioned below ..

–max-load=Threads_running=80
–critical-load=Threads_running=160

With pt-osc, –max-load pauses the copy when load becomes too high, while –critical-load aborts the operation when the configured threshold is exceeded. That gave me a safety model much closer to what I wanted for long-running operations.

The real solution was not pt-osc alone; it was the wrapper around it .. I did not want another DBA typing a 20-line pt-online-schema-change command manually every time. So I wrapped the whole workflow. The generic call became:

AUTO_DROP_OLD=0 \
nohup /defrag/tools/ptosc_restartable_defrag_PREPROD_FINAL.sh \
<SCHEMA> <TABLE> \
> /defrag/tools/<SCHEMA>.<TABLE>.restartable.nohup.out 2>&1 &

That is intentionally simple. The DBA provides: schema and the table and the wrapper deals with everything else.

A normal startup looks like this:
– Restartable pt-osc automation for LAB.big_table
– MySQL=8.0.x
– Threads_running=4
– Threads_connected=820
– PRIMARY KEY columns=id
– Data filesystem free=8.0Ti
– STATE target=1 new=0 old=0 triggers=0
– Selected action=fresh
– Running dry run before fresh restartable job.
– Dry run OK.
– Metadata-lock watchdog started.
– Starting FRESH restartable pt-osc job.

Before pt-osc really starts doing work, the wrapper checks MySQL connectivity and version, current load, filesystem free space, primary-key availability, existing _new and _old objects, leftover pt-osc triggers and previous history entries. The STATE line is one of the most useful parts of the whole implementation.

For example:
target=1
new=0
old=0
triggers=0
resumable_job=none

means I have a clean fresh start. If instead I see:

target=1
new=1
old=0
triggers=3
resumable_job=8

I know there is an interrupted operation that can potentially resume 🙂

Restartability saved me from throwing away hours of work and this became one of the strongest reasons for keeping the pt-osc solution. Suppose a 1 TB copy has been running for 15 hours and somebody kills the process. My wrapper does not blindly remove the shadow table. An interrupted run can look like this:

FAILED rc=137; no blind table/trigger cleanup performed.
POST-PTOSC STATE
target=1
new=1
old=0
triggers=3
resumable_job=8

Copy remains incomplete but is safely resumable. … DO NOT DROP LAB._big_table_new or the pt-osc triggers.

The job history is written to: percona.pt_osc_history … Percona’s –history option stores progress and chunk boundaries, and an unfinished job can be resumed using –resume.
The wrapper therefore knows whether it should perform a fresh start, resume an interrupted copy, or recover from a copy-complete state where cutover was not completed. For a five-minute table, this may not matter. For a table that has already consumed most of your weekend, it matters a lot.

DATA_FREE helped, but it was not the whole truth either ... Yes, that is another surprise was seeing a table with almost no DATA_FREE still reclaim more than 60 GiB after a rebuild.

Before:
Physical size : ~740 GiB
DATA_FREE : ~0.01 GiB
Fresh used : ~732 GiB

Based purely on that data, it did not look very interesting. After pt-osc:

Old table : ~732 GiB
New table : ~671 GiB

Actual reclaim : ~61 GiB .. Why? and this is because DATA_FREE does not tell you how well individual InnoDB B-tree pages are packed. A table can have almost no fully free extents and still have partially occupied pages throughout its clustered and secondary indexes. Rebuilding can pack that data more efficiently. That taught me another important lesson:

DATA_FREE ≈ 0 —> does not necessarily mean: –> rebuild saving = 0

At the same time, a huge difference between stale statistics and the physical file does not mean hundreds of gigabytes are reclaimable either. There is no single magic fragmentation column.

Now comes the monitoring part … How we can monitor the run ?? For long-running jobs I did not want to tail multiple logs and run many commands manually every few minutes. So I wrote a separate monitor and called it a ptosc_monitor.sh Usage is intentionally generic: ./ptosc_monitor.sh 40

That gives me almost everything I need from one screen. The monitor also shows the last lines from the actual pt-osc log and the wrapper log, so I can see whether the operation is copying, throttling, attempting cutover or recovering from an error. I added a metadata-lock watchdog because cutover is where good weekends go bad … A long-running copy can behave perfectly for 20 hours and then fail to cut over because another transaction is holding a metadata lock. .. I did not want to discover that manually. So the wrapper launches a metadata-lock watchdog that checks for blockers while pt-osc is running. The goal is not to kill random application sessions …

The goal is to know: Who is blocking? Which table? How long? Which user? What SQL? and optionally act against known blocker classes after a controlled threshold. All actions are recorded in: killed-blockers.log .. Most of my successful runs reported: “None” .. That is exactly what I want.

Every table execution gets its own directory: /root/ptosc/.// … For example: /root/ptosc/LAB.big_table/20260826_082509/

The important files are:
pt-online-schema-change.log
final-validation.txt
killed-blockers.log
ptosc.pid
pause.flag

The main wrapper output is written separately: /defrag/tools/..restartable.nohup.out

So if somebody asks me a week later: What happened during that 25-hour table rebuild? I do not need to reconstruct the story from bash history. I have the complete execution record.

The biggest lessons from this exercise were simple and are below.

  • MySQL defragmentation is not just a storage calculation followed by OPTIMIZE TABLE.
  • A large .ibd file does not always mean large reclaim. Stale statistics can create false positives, DATA_FREE does not tell the whole story, and even a successful rebuild may reclaim little—or sometimes produce a larger file.
  • Native online DDL is also not automatically controllable. Large operations can generate replication lag, require expensive rollback, and still depend on metadata locks during cutover.
  • For this workload, pt-online-schema-change provided the right foundation: chunking, throttling, restartability, and a predictable shadow-table model.
  • But pt-osc alone was not the solution. The real solution was:
    pt-osc + prechecks + restart logic + disk safeguards + metadata-lock monitoring + replication monitoring + validation + structured logging.
    That turned a database utility into a repeatable production operation.

Resource Authored and Used:

– Monitoring Sxript : https://github.com/fatdba/newprojects/blob/main/MySQL-Defrag-PTOSC-Moniror.sh

– Main Defrag Explanation (Like a ReadmE) : https://github.com/fatdba/newprojects/blob/main/MySQL-Defrag-CompleteScript-PERCONA-PTOSCBased-HELPER
– Main Defrag Script : https://github.com/fatdba/newprojects/blob/main/MySQL-Defrag-CompleteScript-PERCONA-PTOSCBased.sh

Hope It Helped!
Prashant Dixit

Posted in Uncategorized | Leave a Comment »

Saving Oracle unified audit volume with ONLY TOPLEVEL

Posted by FatDBA on August 17, 2026

Unified auditing is one of those features that looks simple when we enable it, but its impact can become very visible very quickly on a busy production system.

I remember once instance where we’d enabled a unified audit policy for a limited set of users. At first glance, the scope looked controlled. It was not enabled for the whole database. It was not auditing every schema. It was not auditing every user. The policy was enabled only for ten named users and only for SELECT activity on a small set of application tables. So, from a distance, it looked safe. But within a short period, the audit trail started growing heavily. In around one day, we saw millions of audit records. That immediately raised the question every DBA asks in this situation —-> “Is something wrong, or is the audit policy doing exactly what we asked it to do?”

In this case, the answer was clear after reviewing the data. Nothing was technically broken. The audit policy was behaving as configured. The issue was that the policy was too broad for the way the application was actually using these tables. The audit policy was defined like this:

CREATE AUDIT POLICY sensitive_read_audit_policy ACTIONS 
  SELECT ON app_schema.sensitive_table_1, 
  SELECT ON app_schema.sensitive_table_2, 
  SELECT ON app_schema.sensitive_table_3, 
  SELECT ON app_schema.sensitive_table_4, 
  SELECT ON app_schema.high_volume_sensitive_table 
  WHEN '1=1' EVALUATE PER STATEMENT;

The important part is this ---> WHEN '1=1' EVALUATE PER STATEMENT

Means that condition is always true. So every qualifying SELECT statement against these audited objects was getting recorded. Since the policy was evaluated per statement, this was not one audit row per user or one audit row per session. It was one audit event per qualifying statement activity. That distinction matters a lot.

On a quiet database, this may not be a concern. But on a busy application system, especially where front-end screens, packages, reports, and repeated lookups are involved, this can quickly generate a very large audit trail. In our case, the audit was enabled only for ten users, but those users were active through the application. The main client program seen in the audit records was TEST.EXE, which appeared to be the primary application front-end client. That was the first clue that the activity was coming from normal application usage, not from random ad hoc access.

The next step was to break down the audit records by object. That changed the whole direction of the analysis … A one-day audit summary showed something like this

OBJECT_SCHEMA   OBJECT_NAME                   ACTION_NAME    AUDIT_RECORDS
-------------   ---------------------------   -----------    -------------
APP_SCHEMA      HIGH_VOLUME_SENSITIVE_TABLE   SELECT           4,425,238
APP_SCHEMA      SENSITIVE_TABLE_1             SELECT              87,785
APP_SCHEMA      SENSITIVE_TABLE_2             SELECT               8,419
APP_SCHEMA      SENSITIVE_TABLE_3             SELECT               2,835
APP_SCHEMA      SENSITIVE_TABLE_4             SELECT               1,263

This changed the direction of the analysis immediately. The issue was not evenly distributed across all audited objects. Almost all of the audit records were coming from one high-volume business table. The high-volume table alone was responsible for roughly 98% of the audit records. That is a very important finding. When audit growth becomes a problem, we should not treat all audited objects equally. One table, one screen, one package, one report, or one application flow may be generating most of the activity.

In this example, the high-volume table had millions of rows and was frequently queried by the application. But table size alone was not the reason for the audit growth. A large table does not generate audit rows by itself. Audit rows are generated when audited activity happens. So the real issue was repeated read activity against that object. From the database side, we can check which client programs or hosts are generating records … something like this

SELECT dbusername,
       client_program_name,
       userhost,
       COUNT(*) AS audit_records
FROM unified_audit_trail
WHERE unified_audit_policies LIKE '%SENSITIVE_READ_AUDIT_POLICY%'
  AND event_timestamp >= SYSTIMESTAMP - INTERVAL '1' DAY
GROUP BY dbusername,
         client_program_name,
         userhost
ORDER BY audit_records DESC;

This helps confirm whether the audit records are coming from normal application usage, reporting tools, ad hoc clients, batch jobs, or unexpected access paths.

In the case behind this post, the majority of audit records were generated by the primary application client. So filtering by that client program would not solve the problem. It would only confirm what we already knew: the main application was the source of the audit volume. We had a few options, FGA clicked instantly, but there are multiple columns out of those tables where they want to enforce auditing, meaning auditing a full table or with FGA will be almost like same, with very less gains as they are looing to audit a wide/huge range of columns and FGA wasn’t a good choice … I mean FGA is more targeted, but If every application query selects the sensitive columns, then FGA may still generate a lot of records. It may make the audit trail more meaningful, but not necessarily small.

Now, next I thought was about one of the option which I see very less DBA discuss about, the ‘ONLY TOPLEVEL’ … Okay, so what this “ONLY TOPLEVEL” does ? … It is an option for unified audit policies that limits audit records to top-level SQL operations. In simple terms, it helps avoid recording the internal or recursive SQL activity that happens underneath a top-level operation. For example, a user may perform one action in the application. That action may call a stored procedure. The procedure may query several tables. The query may access views. The views may access base tables. Oracle may also perform recursive SQL internally. Without top-level filtering, the audit policy may record many of those lower-level operations if they match the audited action. With ONLY TOPLEVEL, the policy focuses on top-level operations instead of recording every indirect SQL statement generated underneath the user action.

ALTER AUDIT POLICY sensitive_read_audit_policy ADD ONLY TOPLEVEL;

This does not change which objects are part of the policy. It changes how much internal activity gets recorded for that policy.

That is why ONLY TOPLEVEL can be very useful when an audit policy is generating a large number of rows because of recursive or package driven SQL. It can reduce audit volume by a great extent. It can reduce pressure on audit storage. It can reduce purge workload. It can also reduce the operational overhead of managing a very large audit trail.

I mean retention is handled by purge. Generation is handled by audit policy design. ONLY TOPLEVEL helps with generation. If it reduces unnecessary internal audit rows, then fewer rows are inserted into the unified audit trail in the first place. It helps in less audit data growth, less pressure on storage, small retentions, less work for purge jobs etc. .. And this is especially important when unified audit data is growing inside system-managed areas such as the audit schema and SYSAUX-related storage. Even if old audit records are purged, physical datafiles may not automatically shrink at the filesystem level. Deleted space may become reusable inside the tablespace, but actual file shrink depends on the datafile high water mark. You can do something like this to implement this change…

-- Check current policy enablement before change

SELECT policy_name,
       enabled_option,
       entity_name,
       entity_type,
       success,
       failure
FROM audit_unified_enabled_policies
WHERE policy_name = 'SENSITIVE_READ_AUDIT_POLICY' ORDER BY entity_name;

NOAUDIT POLICY sensitive_read_audit_policy BY user_1, user_2, user_3;
ALTER AUDIT POLICY sensitive_read_audit_policy ADD ONLY TOPLEVEL;

-- Reverify the policy
SELECT policy_name,
       enabled_option,
       entity_name,
       entity_type,
       success,
       failure
FROM audit_unified_enabled_policies
WHERE policy_name = 'SENSITIVE_READ_AUDIT_POLICY' ORDER BY entity_name;

Soon after we implemented the ONLY TOPLEVEL approach, we saw huge drop in audit records generation. It was more than 90% reduction in records 🙂 And all top level information about underlying sensitive obejcts were well captured by the auditing after ignoring all recursive statements.

The biggest lesson from this exercise is that audit volume should be understood before it becomes a storage or performance issue. A policy can be logically correct and still operationally expensive. Auditing a few users and a few objects may sound small, but if one high-volume table is read millions of times through normal application activity, the unified audit trail can grow very quickly. ONLY TOPLEVEL is a very useful option when the audit policy is capturing too much internal or recursive SQL activity. WHENEVER SUCCESSFUL helps align the policy with a successful-read requirement. FGA can make the design more targeted when specific columns or conditions matter.

But the best audit design starts with a simple question –> What exact evidence do we need? … If the requirement is database object access, unified auditing may be enough. If the requirement is business record access, such as which account, transaction, investment, or bank record was viewed, then application or package-level audit logging may be needed as well.

A good audit trail is not the biggest audit trail. A good audit trail is the one that captures the right evidence, at the right level, with a volume the system can safely manage.

Hope It Helped!
Prashant Dixit

Posted in Uncategorized | Tagged: , , , , | Leave a Comment »

Loading an ONNX embedding model into Oracle AI Database 26ai

Posted by FatDBA on August 11, 2026

As part of a recent AI and RAG proof of concept, I needed to load the all-MiniLM-L12-v2 embedding model directly into Oracle AI Database 26ai. The process was straightforward, although I encountered one small connection issue that is worth documenting.

ONNX stands for Open Neural Network Exchange. It is an open format for representing machine-learning models independently of the framework in which they were created. For example, a model may be trained using PyTorch and then exported to ONNX for optimized inference in another environment. This makes the model more portable and avoids tying it permanently to one machine-learning framework. In Oracle AI Database 26ai, an ONNX embedding model can be loaded as a database object and called directly from SQL. Text can therefore be converted into vectors without sending it to an external AI service.

Running the embedding model inside the database provides several advantages:

  • Sensitive text remains inside the database.
  • No external REST API or internet connection is required during inference.
  • Network latency and API usage costs are avoided.
  • Embeddings can be generated directly through SQL and PL/SQL.
  • The model can be used consistently for document and search-query embeddings.
  • Deployment becomes easier because a separate embedding service is not required.

For this test, I used all-MiniLM-L12-v2. It is a relatively small and efficient English-language model that produces 384-dimensional vectors, making it a good choice for semantic search, document retrieval and lightweight RAG solutions.

A wquick comparison between difference approaches and why and when ONNX can become a preferred choice. ONNX is particularly useful when data privacy, predictable performance and simplified architecture are more important than continuously switching to the latest hosted embedding model.

ApproachMain advantageMain consideration
In-database ONNXPrivate, fast and no external API dependencyUses database CPU and memory
External embedding APIEasy access to newer hosted modelsNetwork latency, API cost and data-privacy considerations
PyTorch/TensorFlow serviceMaximum flexibility and fine-tuning optionsRequires a separate runtime and serving infrastructure

Below are the steps to download and load the model … Oracle provides an augmented version of all-MiniLM-L12-v2 that includes the required tokenization and post-processing logic. This is important because a raw ONNX model downloaded directly from Hugging Face may contain only the neural network and may not be sufficient for direct in-database text embedding.

[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ ps -ef|grep pmon
oracle     36866    4602  0 03:37 ?        00:00:21 ora_pmon_myaidb
oracle     92325   92163  0 17:14 pts/0    00:00:00 grep --color=auto pmon
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ . oraenv
ORACLE_SID = [oracle] ? myaidb
The Oracle base has been set to /u01/app/oracle
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ !sql
sqlplus / as sysdba

SQL*Plus: Release 23.26.1.0.0 - Production on Tue Aug 11 17:14:11 2026
Version 23.26.1.0.0

Copyright (c) 1982, 2025, Oracle.  All rights reserved.


Connected to:
Oracle AI Database 26ai Enterprise Edition Release 23.26.1.0.0 - Production
Version 23.26.1.0.0

SQL>
SQL>
SQL> exit
Disconnected from Oracle AI Database 26ai Enterprise Edition Release 23.26.1.0.0 - Production
Version 23.26.1.0.0
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ sudo mkdir -p /u01/app/oracle/onnx_models
[sudo] password for oracle:
oracle is not in the sudoers file.  This incident will be reported.
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ cd /u01/app/oracle/onnx_models
-bash: cd: /u01/app/oracle/onnx_models: No such file or directory
[oracle@fatdba1 ~]$  mkdir -p /u01/app/oracle/onnx_models
[oracle@fatdba1 ~]$
[oracle@fatdba1 ~]$ cd /u01/app/oracle/onnx_models
[oracle@fatdba1 onnx_models]$ wget https://adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com/p/TtH6hL2y25EypZ0-rrczRZ1aXp7v1ONbRBfCiT-BDBN8WLKQ3lgyW6RxCfIFLdA6/n/adwc4pm/b/OML-ai-models/o/all_MiniLM_L12_v2_augmented.zip
--2026-08-11 17:16:56--  https://adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com/p/TtH6hL2y25EypZ0-rrczRZ1aXp7v1ONbRBfCiT-BDBN8WLKQ3lgyW6RxCfIFLdA6/n/adwc4pm/b/OML-ai-models/o/all_MiniLM_L12_v2_augmented.zip
Resolving adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com (adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com)... 134.70.24.1, 134.70.32.1, 134.70.28.1
Connecting to adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com (adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com)|134.70.24.1|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 122537890 (117M) [application/x-zip-compressed]
Saving to: ‘all_MiniLM_L12_v2_augmented.zip’

all_MiniLM_L12_v2_augmented.zip                 100%[====================================================================================================>] 116.86M  7.92MB/s    in 19s

2026-08-11 17:17:17 (6.09 MB/s) - ‘all_MiniLM_L12_v2_augmented.zip’ saved [122537890/122537890]

[oracle@fatdba1 onnx_models]$
[oracle@fatdba1 onnx_models]$ ls -lh /u01/app/oracle/onnx_models
total 117M
-rw-r--r--. 1 oracle oinstall 117M Oct 30  2025 all_MiniLM_L12_v2_augmented.zip
[oracle@fatdba1 onnx_models]$ unzip all_MiniLM_L12_v2_augmented.zip
Archive:  all_MiniLM_L12_v2_augmented.zip
  inflating: all_MiniLM_L12_v2.onnx
  inflating: LICENSE_ATTRIBUTION.txt
  inflating: README-ALL_MINILM_L12_V2-augmented.txt
[oracle@fatdba1 onnx_models]$ ls -ltrh
total 245M
-rw-rw-rw-. 1 oracle oinstall 4.2K Oct 30  2025 README-ALL_MINILM_L12_V2-augmented.txt
-rw-rw-rw-. 1 oracle oinstall  12K Oct 30  2025 LICENSE_ATTRIBUTION.txt
-rw-rw-rw-. 1 oracle oinstall 128M Oct 30  2025 all_MiniLM_L12_v2.onnx
-rw-r--r--. 1 oracle oinstall 117M Oct 30  2025 all_MiniLM_L12_v2_augmented.zip
[oracle@fatdba1 onnx_models]$ sqlplus / as sysdba

SQL*Plus: Release 23.26.1.0.0 - Production on Tue Aug 11 17:20:34 2026
Version 23.26.1.0.0

Copyright (c) 1982, 2025, Oracle.  All rights reserved.


Connected to:
Oracle AI Database 26ai Enterprise Edition Release 23.26.1.0.0 - Production
Version 23.26.1.0.0

SQL> SHOW PDBS;

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO
         3 MYYAIDB                        READ WRITE NO
SQL> ALTER SESSION SET CONTAINER = MYYAIDB;

Session altered.

SQL> SHOW CON_NAME;

CON_NAME
------------------------------
MYYAIDB
SQL> CREATE USER vector_user IDENTIFIED BY oracle90 DEFAULT TABLESPACE users QUOTA UNLIMITED ON users;

User created.

SQL> GRANT DB_DEVELOPER_ROLE TO vector_user;

Grant succeeded.

SQL> GRANT CREATE MINING MODEL TO vector_user;

Grant succeeded.

SQL> CREATE OR REPLACE DIRECTORY ONNX_DIR AS '/u01/app/oracle/onnx_models';

Directory created.

SQL> GRANT READ, WRITE ON DIRECTORY ONNX_DIR TO vector_user;

Grant succeeded.

SQL> SELECT directory_name, directory_path
FROM dba_directories
WHERE directory_name = 'ONNX_DIR';  2    3

DIRECTORY_NAME
--------------------------------------------------------------------------------
DIRECTORY_PATH
--------------------------------------------------------------------------------
ONNX_DIR
/u01/app/oracle/onnx_models


SQL> 



SQL> CONNECT vector_user@"//127.0.0.1:1521/myyaidb"
Enter password:
Connected.
SQL>
SQL> SHOW USER;
SHOW CON_NAME;USER is "VECTOR_USER"
SQL>

CON_NAME
------------------------------
MYYAIDB
SQL> BEGIN
  DBMS_VECTOR.LOAD_ONNX_MODEL(
    directory  => 'ONNX_DIR',
    file_name  => 'all_MiniLM_L12_v2.onnx',
    model_name => 'ALL_MINILM_L12_V2'
  );
END;
/  2    3    4    5    6    7    8

PL/SQL procedure successfully completed.

SQL> COLUMN model_name FORMAT A30
COLUMN mining_function FORMAT A20
COLUMN algorithm FORMAT A20

SELECT model_name,
       mining_function,
       algorithm
FROM user_mining_models
WHERE model_name = 'ALL_MINILM_L12_V2';SQL> SQL> SQL> SQL>   2    3    4    5

MODEL_NAME                     MINING_FUNCTION      ALGORITHM
------------------------------ -------------------- --------------------
ALL_MINILM_L12_V2              EMBEDDING            ONNX

SQL> SELECT VECTOR_DIMS(
         VECTOR_EMBEDDING(
           ALL_MINILM_L12_V2
           USING 'Oracle AI Vector Search test' AS DATA
         )
       ) AS dimensions
FROM dual;  2    3    4    5    6    7

DIMENSIONS
----------
       384

SQL> SET LONG 100000
SET LINESIZE 200

SELECT VECTOR_EMBEDDING(
         ALL_MINILM_L12_V2
         USING 'Oracle Database backup and recovery' AS DATA
       ) AS embedding
FROM dual;SQL> SQL> SQL>   2    3    4    5

EMBEDDING
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
[-4.87856008E-002,7.1997717E-002,-2.54948232E-002,-5.70283532E-002,-1.06920907E-002,1.0775161E-002,-5.90280592E-002,4.400618E-002,-4.38417606E-002,-1.99456103E-002,5.76283038E-003,7.40298927E-002,1.31
917335E-002,-3.32452208E-002,-8.51406157E-002,-2.12764088E-002,-8.46216164E-004,6.76434338E-002,-5.09454496E-003,7.59954751E-002,-1.47132769E-001,-7.61819351E-003,4.72804867E-002,-3.58171314E-002,4.77
031525E-003,9.91140381E-002,6.09872341E-002,-4.96129356E-002,-6.89210966E-002,-2.29366459E-002,3.0035438E-002,-8.5439207E-003,-1.87823065E-002,-5.27824089E-002,-5.07728159E-002,5.71492985E-002,-1.0317
5268E-001,2.06469093E-003,-4.94726524E-002,5.38232923E-002,-4.20075236E-003,-5.83661646E-002,-6.939473E-002,4.70481254E-003,-3.57628129E-002,-2.06067171E-002,1.55578945E-002,-3.64977755E-002,1.3607616
5E-002,4.83786911E-002,6.55189529E-002,9.09776092E-002,-3.78638739E-003,4.63182144E-002,1.67185273E-002,-2.0538846E-002,1.88092468E-003,1.13089666E-001,-6.65725395E-002,-5.29151671E-002,7.0534341E-002
,3.82151194E-002,-3.87142561E-002,4.89930958E-002,-3.72733884E-002,4.71422225E-002,-9.42653418E-003,8.30410421E-002,7.23783001E-002,-8.00792221E-003,-1.04699969E-001,1.3227962E-002,2.10432312E-003,-5.
2194491E-002,5.44612296E-002,2.64221951E-002,-6.92174537E-003,3.2788564E-003,-7.07466304E-002,-1.09599065E-002,-2.07525976E-002,6.07146174E-002,-7.14476183E-002,-6.52319693E-004,-2.61781225E-003,1.029
82512E-002,2.22747419E-002,7.84466136E-003,-2.85771638E-002,-7.22757652E-002,1.31514639E-001,-6.92670466E-003,1.05359353E-001,2.31848098E-002,2.36764625E-002,1.93412453E-002,-7.73303732E-002,-3.468839
68E-003,1.3238284E-001,2.77728699E-002,6.44600466E-002,2.86891386E-002,3.98902521E-002,-1.04312496E-002,-1.1179284E-001,1.21374004E-001,5.50982319E-002,-2.86319014E-002,-7.31713325E-002,2.7698854E-002
,-7.78927729E-002,7.7220737E-003,4.11929712E-002,1.38421329E-002,2.2667855E-002,-9.06801131E-003,-1.06181979E-001,-1.63826789E-003,-8.82744044E-002,-1.22012058E-003,1.58981606E-002,-7.58664086E-002,9.
57456082E-002,1.25050836E-003,5.26473019E-003,2.16946639E-002,4.42689583E-002,6.8870157E-002,1.41299153E-002,5.51517941E-002,-5.14429659E-002,-5.23285232E-002,5.11708781E-002,-2.63342485E-002,-2.20014

EMBEDDING
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
565E-002,-1.73898265E-002,-3.62575054E-002,-8.39175209E-002,1.63958769E-003,-6.85187057E-002,2.91524939E-002,-1.2798515E-001,-1.83849374E-003,7.93192387E-002,2.51277834E-002,4.95435148E-002,-4.4572427
9E-002,-3.21206041E-002,-6.48666397E-002,2.0290127E-002,6.15623547E-003,2.16576364E-002,-4.66784462E-003,4.63866107E-002,3.30464952E-002,3.85379605E-002,1.23593844E-002,-4.7582306E-002,-7.36421198E-00
2,-7.33523369E-002,-6.89318478E-002,-7.00623095E-002,-3.05234399E-002,7.80285448E-002,-9.35370028E-002,2.69017499E-002,-1.14727495E-002,-2.12617069E-002,4.35556322E-002,-4.36026894E-004,9.77274105E-00
2,-3.76839899E-002,-8.01233053E-002,-7.22195879E-002,3.63463326E-003,-2.48486884E-002,-2.03172676E-002,-2.47620214E-002,2.27805087E-003,-5.56117743E-002,7.34143425E-003,-5.76570295E-002,-4.28294996E-0
03,5.3869877E-002,2.87791248E-002,5.07858656E-002,-5.34493811E-002,8.79583731E-002,7.63130933E-002,4.32174765E-002,1.31380977E-002,-3.26855257E-002,1.11090411E-002,-4.76806909E-002,-4.36781952E-003,1.
01589016E-003,3.21580544E-002,1.30654527E-002,3.04144267E-002,-3.80649902E-002,-4.27651405E-002,4.52963784E-002,-1.58415572E-003,-1.993821E-002,3.90723571E-002,-4.20001186E-002,-1.84409693E-002,9.7515
6799E-002,-4.25614715E-002,-4.88893613E-002,-4.92014624E-002,9.41627845E-003,8.72373432E-002,4.78187315E-002,-2.54044053E-003,-3.99130322E-002,1.34559376E-002,6.15696721E-002,-2.59792339E-002,-4.70647
514E-002,1.11222304E-001,-6.54862896E-002,9.61392298E-002,2.07331398E-034,-3.65259964E-003,-8.21233988E-002,-1.20636057E-002,1.44557646E-002,-3.51657383E-002,-1.3823634E-002,-1.64992362E-002,4.4988017
5E-002,-4.54784371E-002,-2.92942306E-004,2.15027835E-002,-1.31722605E-002,1.60137545E-002,-6.75500855E-002,-4.15628366E-002,-7.30369315E-002,8.64059255E-002,-5.12768142E-002,-3.84957977E-002,4.5663215
2E-002,6.85658455E-002,4.10435982E-002,7.44328089E-003,8.91532004E-002,2.70864032E-002,3.0453993E-002,-3.20769548E-002,1.2025306E-002,-1.22540602E-002,8.77800658E-002,3.16540003E-002,-8.52044672E-003,
-5.95438443E-002,1.48355916E-001,-3.60100642E-002,-1.0032624E-002,2.08637211E-002,2.84589995E-002,-2.41973568E-002,3.15869339E-002,1.12110479E-002,5.38171502E-004,2.05618907E-002,1.07340226E-002,-1.28

EMBEDDING
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
460256E-003,-5.25169186E-002,2.49615274E-002,1.5979778E-002,7.37336874E-002,-2.18623616E-002,8.46073851E-002,-2.29403824E-002,5.97928651E-002,2.6352426E-002,8.22185166E-003,-3.29606682E-002,9.63416323
E-002,-8.68180543E-002,-4.90903854E-002,2.07517482E-002,-2.32356545E-002,-6.64651319E-002,2.38323156E-002,5.20869419E-002,-1.13063902E-002,-1.03914761E-003,1.15302512E-002,5.39318845E-002,-2.76873186E
-002,-7.64334872E-002,1.62822269E-002,-4.78982665E-002,-7.46970624E-002,4.14117053E-002,1.85170472E-002,1.50091248E-002,-2.5248073E-002,5.56167169E-003,5.31971306E-002,4.19711061E-002,-1.05979659E-001
,-2.56049028E-003,-8.31877738E-002,5.11595644E-002,-3.61793442E-003,2.72909719E-002,2.98573133E-002,-1.31529346E-001,5.84768467E-002,-8.44340324E-002,-2.63655409E-002,-1.02462331E-002,-3.13680619E-002
,2.79930402E-002,7.4749887E-002,-6.41344061E-033,-4.88035977E-002,3.4346763E-002,1.05161041E-001,1.83394493E-003,5.63648827E-002,-1.1864084E-001,2.758242E-002,4.09473255E-002,-3.94932777E-002,-5.41642
867E-002,-8.78240447E-003,5.86595088E-002,3.33358464E-003,9.50002074E-002,-4.01680171E-002,7.73140565E-002,1.7790196E-003,-2.23562624E-002,-1.65800732E-002,-6.10178225E-002,3.96786481E-002,-5.95294759
E-002,4.85839993E-002,-9.53975134E-003,5.50697884E-003,3.7492007E-002,5.52483797E-002,2.59457417E-002,3.2463897E-002,-1.12312446E-004,1.87224504E-002,5.46318106E-002,-1.953681E-002,4.85305414E-002,7.7
0374667E-003,-1.2780644E-001,3.89365392E-004,9.75998677E-003,-8.70148391E-002,2.54446249E-002,8.90563652E-002,7.0743598E-002,-4.44219634E-002,-2.12613344E-002,-5.60087189E-002,2.35729497E-002,2.095438
73E-002,1.70082841E-002,-9.77526943E-005,-4.19775173E-002,2.22535385E-003,7.9500908E-003,-3.70596088E-002,2.16165632E-002,2.07185978E-003,-6.042099E-002,1.44844491E-003,6.97642863E-002,3.27955447E-002
,-5.20859994E-002,3.89892906E-002,4.39782776E-002,-1.08199725E-002,-7.10714981E-002]


SQL>

And that’s it. Its simple. Oracle AI Database 26ai makes it surprisingly easy to bring embedding generation directly to the data. Once the augmented ONNX model is staged and loaded, embeddings can be generated through regular SQL without maintaining a separate Python service or calling an external API.

For organizations building private semantic-search or RAG solutions, in-database ONNX provides a practical balance of security, performance and operational simplicity.

Hope It Helped!
Prashant Dixit

Posted in Uncategorized | Tagged: , , , , , , , , , , | Leave a Comment »