From AI Models to Semantic Search Using VECTOR Data Type and VECTOR_DISTANCE
For decades, Oracle Database has been the platform of choice for mission-critical transactional and analytical workloads. With Oracle Database 23ai and the Oracle Database 26ai, Oracle has taken a significant leap into the AI era by introducing native Vector Search capabilities directly inside the database engine.
Instead of moving data to external vector databases, Oracle now enables organizations to:
Combine traditional SQL filtering with AI-powered semantic retrieval
This dramatically simplifies AI application architectures while leveraging Oracle’s strengths in security, scalability, availability, and SQL optimization.
In this article, we will perform a complete end-to-end walkthrough:
Understanding Vector Search
Choosing the right embedding model
Loading AI models into Oracle
Generating vectors using the VECTOR datatype
Performing semantic searches using VECTOR_DISTANCE
Understanding Vector Search
Traditional database searches rely on exact matches.
For example:
SELECT *
FROM documents
WHERE document_text LIKE '%database%';
This query only finds documents containing the exact word “database”.
What if the document contains:
Oracle AI Platform
Data Management System
Information Repository
Traditional searches may miss relevant content.
Vector Search solves this problem.
The AI model converts text into a mathematical representation called an embedding vector.
Such as:
"Oracle Database"
↓
[0.234, -0.876, 0.123, ...]
Semantically similar phrases generate vectors that are close together in vector space, like:
"Oracle Database"
"Oracle RDBMS"
"Oracle Data Platform"
Their vectors are positioned near each other. The database can then calculate similarity mathematically.
Oracle AI Vector Architecture
The overall workflow looks like:
Document
↓
Embedding Model
↓
VECTOR datatype
↓
Vector Index
↓
VECTOR_DISTANCE
↓
Semantic Search Results
Oracle 23ai introduces:
VECTOR datatype
Vector indexes
ONNX model support
DBMS_VECTOR package
VECTOR_DISTANCE functions
Oracle 26ai further enhances:
Hybrid Search
Improved ANN indexing
GPU acceleration integrations
Enhanced AI workflows
Better model management
Choosing the Right Embedding Model
The quality of your Vector Search depends heavily on the embedding model. A poor model produces poor search results.A good model creates semantically meaningful vectors.
Popular Embedding Models
all-MiniLM-L6-v2
Dimension: 384
Advantages:
Small
Fast
Low memory footprint
Excellent for demos and production workloads
Use Cases:
FAQ search
Knowledge bases
RAG applications
bge-small-en
Dimension: 384
Advantages:
Better semantic understanding
Higher retrieval quality
Use Cases:
Enterprise Search
Internal Documentation
bge-large-en
Dimension: 1024
Advantages:
Excellent accuracy
Disadvantages:
Higher storage requirements
Larger vectors
Use Cases:
High-quality semantic retrieval
multilingual-e5-large
Dimension:1024
Advantages:
Multi-language support
Ideal for:
Global organizations
Selecting a Model
General recommendation:
Workload
Recommended Model
Demo
all-MiniLM-L6-v2
RAG
bge-small-en
Enterprise Search
bge-large-en
Multilingual
e5-large
For most Oracle implementations:
all-MiniLM-L6-v2
provides the best balance between performance and accuracy.
Loading an ONNX Model into Oracle
Oracle supports ONNX models natively.
Suppose we downloaded:
all_MiniLM_L6_v2.onnx
Create a directory:
CREATE OR REPLACE DIRECTORY AI_MODELS AS
'/u01/models';
Grant access:
GRANT READ, WRITE ON DIRECTORY AI_MODELS TO vector_user;
Modern OLTP applications frequently suffer from blocking sessions caused by long-running or abandoned transactions. In earlier Oracle releases, DBAs typically had to manually identify and kill blocking sessions using commands such as:
ALTER SYSTEM KILL SESSION 'sid,serial#';
This approach disrupts application connectivity, impacts user sessions, and increases operational overhead.
Oracle Database 23ai introduces a powerful feature called Priority Transactions (also referred to as Automatic Transaction Rollback). This feature allows Oracle to automatically roll back lower-priority transactions when they block higher-priority transactions for longer than a configured threshold.
The major benefit:
High-priority business transactions continue processing.
Blocking transactions are automatically rolled back.
Sessions remain alive.
Applications can gracefully recover.
DBA intervention is reduced.
How Priority Transactions Work
Oracle introduces:
1. Transaction Priorities
Each session can be assigned one of the following priorities:
Priority
Description
HIGH
Never auto-rolled back
MEDIUM
Can be rolled back by HIGH
LOW
Can be rolled back by HIGH or MEDIUM
The priority is assigned at session level:
ALTER SESSION SET txn_priority = HIGH;
2. Wait Threshold Parameters
These parameters define how long a higher-priority transaction waits before Oracle automatically rolls back the blocker.
Parameter
Purpose
PRIORITY_TXNS_HIGH_WAIT_TARGET
Wait time for HIGH priority transaction
PRIORITY_TXNS_MEDIUM_WAIT_TARGET
Wait time for MEDIUM priority transaction
For example:
ALTER SYSTEM SET priority_txns_high_wait_target = 10;
This means if a HIGH priority transaction waits longer than 10 seconds on a row lock held by a LOW or MEDIUM transaction, Oracle automatically rolls back the blocking transaction.
TRACK mode is useful before enabling automatic rollback in production. Instead of rolling back transactions, Oracle only tracks potential rollback events.
Enable TRACK mode as follows:
SQL> ALTER SYSTEM SET priority_txns_mode = TRACK;
SQL>
Now Oracle records statistics without actually terminating transactions. Following are the useful statistics:
SELECT name, value
FROM v$sysstat
WHERE name LIKE '%priority%';
Important Production Considerations
I) Do Not Overuse HIGH Priority
If application only uses HIGH priority:
No transaction can be auto-rolled back
The feature becomes ineffective
Use priorities strategically.
II) Applications Must Handle Rollbacks
Applications should:
Catch ORA-63300
Catch ORA-63302
Retry transactions safely
Issue explicit ROLLBACK
III) Choose Wait Targets Carefully
Very small wait targets can cause excessive rollbacks.
/usr/sbin/mysqld Ver 8.4.8-commercial for Linux on x86_64 (MySQL Enterprise Server - Commercial)
[mysqladm@prim03 ]$
[mysqladm@prim03 ]$ sudo systemctl start mysqld
[mysqladm@prim03 ]$
5.1 Verify node rejoins cluster
Inside mysqlsh:
cluster.status({extended:1})
6. Upgrade Secondary Node prim02
Repeat same process as mentioned in poing 3 and 5.
Verify the cluster status and notice prim02 and prim03 are now on version “8.4.8” and primary prim01 is still on version “8.4.6”.
MySQL localhost:3306 ssl JS > \c clust_admin@localhost:3306
MySQL localhost:3306 ssl JS > dba.getCluster().status()
{
"clusterName": "testclust",
"defaultReplicaSet": {
"name": "default",
"primary": "prim01:3306",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"prim01:3306": {
"address": "prim01:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.6"
},
"prim02:3306": {
"address": "prim02:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim03:3306": {
"address": "prim03:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "prim01:3306"
}
MySQL localhost:3306 ssl JS >
7. Switchover PRIMARY Away from prim01
Important: You cannot always promote a higher patch-version node over a lower-version primary using cluster.setPrimaryInstance() during rolling upgrades. Community reports confirm version ordering restrictions during in-group rolling upgrades.
Best practice:
Stop Group Replication on old primary
Allow automatic election
7.1 Stop Group Replication on prim01
Connect to prim01:
STOP GROUP_REPLICATION;
[mysqladm@prim01 ]$ sudo systemctl stop mysqld
[mysqladm@prim01 ]$
The cluster automatically elects
prim02 OR prim03 as new PRIMARY and application should continue working through Router.
7.2 Verify new PRIMARY
On prim02 or prim03 connect to mysql shell and check the cluster status
MySQL localhost:3306 ssl JS > \c clust_admin@localhost:3306
MySQL localhost:3306 ssl JS > dba.getCluster().status()
{
"clusterName": "testclust",
"defaultReplicaSet": {
"name": "default",
"primary": "prim02:3306",
"ssl": "REQUIRED",
"status": "OK_NO_TOLERANCE_PARTIAL",
"statusText": "Cluster is NOT tolerant to any failures. 1 member is not active.",
"topology": {
"prim01:3306": {
"address": "prim01:3306",
"memberRole": "SECONDARY",
"mode": "n/a",
"readReplicas": {},
"role": "HA",
"status": "(MISSING)
},
"prim02:3306": {
"address": "prim02:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim03:3306": {
"address": "prim03:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "prim01:3306"
}
MySQL localhost:3306 ssl JS >
8. Upgrade Former PRIMARY prim01
Connect to prim01 and repeat same process as mentioned in poing 3 and 5.
Once completed, verify the cluster status and notice all the nodes prim01,prim02, and prim03 are now on version “8.4.8”
MySQL localhost:3306 ssl JS > dba.getCluster().status()
{
"clusterName": "testclust",
"defaultReplicaSet": {
"name": "default",
"primary": "prim03:3306",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"prim01:3306": {
"address": "prim01:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim02:3306": {
"address": "prim02:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim03:3306": {
"address": "prim03:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "prim03:3306"
}
MySQL localhost:3306 ssl JS >
9. Upgrade MySQL Router on prodapp
Login to application host “prodapp” and install the router binaries.
So far, we have built a highly available 3-node MySQL 8.4 InnoDB Cluster with MySQL Router handling intelligent application routing. You may refer it here
But what happens when your infrastructure requirements evolve further?
What if you need:
Disaster Recovery (DR)
Cross-region replication
Geo-distributed reads
Regional failover
Data center isolation
Multi-site redundancy
This is where Replica Clusters become incredibly powerful.
MySQL 8.4 allows you to extend an existing InnoDB Cluster into a ClusterSet-style topology using:
cluster.createReplicaCluster()
This enables an entirely separate InnoDB Cluster to replicate asynchronously from the primary cluster while still preserving high availability within each site.
What Is a Replica Cluster?
A Replica Cluster is:
a fully independent InnoDB Cluster
with its own Group Replication quorum
its own PRIMARY node
its own failover handling
asynchronously replicating from another cluster
Think of it as:
“A cluster replicating from another cluster.”
Component
Purpose
Primary Cluster
Main production workload
Replica Cluster
DR / remote site / reporting / geo-redundancy
Prod Site
Host
Role
prim01
PRIMARY
prim02
SECONDARY
prim03
SECONDARY
DR Site
Host
Role
stand01
PRIMARY
stand02
SECONDARY
stand03
SECONDARY
Router Nodes
Host
Role
prodapp
Primary Router
drapp
DR Router
Why Replica Clusters Matter
Traditional async replication usually introduces:
single-node replicas
fragile failover processes
replication management complexity
Replica Clusters solve this elegantly.
Benefits
Site-Level High Availability
Even if an entire DR node fails, the DR cluster remains operational.
Regional Disaster Recovery
Protect against:
data center outages
network isolation
cloud region failures
Read Scaling Across Regions
Remote users can read from geographically closer replica clusters.
Reduced Operational Complexity
You manage clusters instead of dozens of individual replicas.
Preparing the Replica Cluster
Before creating a Replica Cluster, you must first build another standalone InnoDB Cluster.
This means repeating the earlier setup steps on:
stand01
stand02
stand03
Including:
MySQL installation
GTID configuration
MySQL Shell setup
Group Replication configuration
Cluster creation
Create the DR InnoDB ClusterSet
Connect to the primary prod instance and check the cluster status:
[vagrant@prim01 ~]$ mysqlsh --js
MySQL Shell 8.4.9
MySQL JS > \c clust_admin@localhost:3306
Creating a session to 'clust_admin@localhost:3306'
Fetching schema names for auto-completion... Press ^C to stop.
Your MySQL connection id is 12
Server version: 8.4.8 Source distribution
No default schema selected; type \use <schema> to set one.
MySQL localhost:3306 ssl JS >
MySQL localhost:3306 ssl JS > var cluster=dba.getCluster()
MySQL localhost:3306 ssl JS > cluster.status();
{
"clusterName": "prod",
"defaultReplicaSet": {
"name": "default",
"primary": "prim01:3306",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"prim01:3306": {
"address": "prim01:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim02:3306": {
"address": "prim02:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim03:3306": {
"address": "prim03:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.9"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "prim01:3306"
}
MySQL localhost:3306 ssl JS >
Now lets create a Replica Cluster “DRCluster”
MySQL localhost:3306 ssl JS > cluster.createReplicaCluster('clust_admin@stand01:3306', 'DRCluster', {recoveryMethod: 'clone'});
Setting up replica 'DRCluster' of cluster 'prod' at instance 'Stand01:3306'.
A new InnoDB Cluster will be created on instance 'Stand01:3306'.
Validating instance configuration at stand01:3306...
This instance reports its own address as Stand01:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'Stand01:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
* Checking transaction state of the instance...
* Checking connectivity and SSL configuration to PRIMARY Cluster...
Monitoring Clone based state recovery of the new member. Press ^C to abort the operation.
Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.
* Waiting for clone to finish...
NOTE: Stand01:3306 is being cloned from prim01:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############################################################ 100% Completed
PAGE COPY ############################################################ 100% Completed
REDO COPY ############################################################ 100% Completed
* Clone process has finished: 73.68 MB transferred in about 1 second (~73.68 MB/s)
Creating InnoDB Cluster 'DRCluster' on 'Stand01:3306'...
Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.
Cluster "memberAuthType" is set to 'PASSWORD' (inherited from the ClusterSet).
* Configuring ClusterSet managed replication channel...
** Changing replication source of Stand01:3306 to prim01:3306
* Waiting for instance 'Stand01:3306' to synchronize with PRIMARY Cluster...
** Transactions replicated ############################################################ 100%
* Updating topology
* Waiting for the Cluster to synchronize with the PRIMARY Cluster...
** Transactions replicated ############################################################ 100%
Replica Cluster 'DRCluster' successfully created on ClusterSet 'ProdClusterSet'.
<Cluster:DRCluster>
MySQL localhost:3306 ssl JS >
What Happens Internally?
This single command performs a huge amount of orchestration automatically.
Internally MySQL Will:
validate DR cluster health
establish asynchronous replication
configure recovery channels
synchronize GTID positions
create replication accounts
register cluster metadata
establish ClusterSet relationships
This is dramatically simpler than manually configuring:
asynchronous replication
relay logs
replication users
failover metadata
topology management
Connect to DR cluster’s primary node “stand01” and check the cluster status of DR
MySQL localhost:3306 ssl JS > \c clust_admin@stand01:3306
Creating a session to 'clust_admin@stand01:3306'
Please provide the password for 'clust_admin@stand01:3306': ***********
Save password for 'clust_admin@stand01:3306'? [Y]es/[N]o/Ne[v]er (default No): Y
Fetching schema names for auto-completion... Press ^C to stop.
Closing old connection...
Your MySQL connection id is 846
Server version: 8.4.9 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
MySQL stand01:3306 ssl JS > var dr = dba.getCluster()
MySQL stand01:3306 ssl JS > dr.status()
{
"clusterName": "DRCluster",
"clusterRole": "REPLICA",
"clusterSetReplicationStatus": "OK",
"defaultReplicaSet": {
"name": "default",
"primary": "Stand01:3306",
"ssl": "REQUIRED",
"status": "OK_NO_TOLERANCE",
"statusText": "Cluster is NOT tolerant to any failures.",
"topology": {
"Stand01:3306": {
"address": "Stand01:3306",
"memberRole": "PRIMARY",
"mode": "R/O",
"readReplicas": {},
"replicationLagFromImmediateSource": "",
"replicationLagFromOriginalSource": "",
"role": "HA",
"status": "ONLINE",
"version": "8.4.9"
}
},
"topologyMode": "Single-Primary"
},
"domainName": "ProdClusterSet",
"groupInformationSourceMember": "Stand01:3306",
"metadataServer": "prim01:3306"
}
MySQL stand01:3306 ssl JS >
So far only Stand01 node is the part of DRCluster. Lets add Node02 “Stand02”
MySQL stand01:3306 ssl JS > dr.addInstance('clust_admin@stand02:3306',{recoveryMethod: 'clone'})
Clone based recovery selected through the recoveryMethod option
Validating instance configuration at stand02:3306...
This instance reports its own address as Stand02:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'Stand02:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
A new instance will be added to the InnoDB Cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.
Adding instance to the cluster...
* Waiting for the Cluster to synchronize with the PRIMARY Cluster...
** Transactions replicated ############################################################ 100%
* Configuring ClusterSet managed replication channel...
** Changing replication source of Stand02:3306 to prim01:3306
Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.
* Waiting for clone to finish...
NOTE: Stand02:3306 is being cloned from stand01:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############################################################ 100% Completed
PAGE COPY ############################################################ 100% Completed
REDO COPY ############################################################ 100% Completed
NOTE: Stand02:3306 is shutting down...
* Waiting for server restart... ready
* Stand02:3306 has restarted, waiting for clone to finish...
** Stage RESTART: Completed
* Clone process has finished: 80.00 MB transferred in about 1 second (~80.00 MB/s)
State recovery already finished for 'Stand02:3306'
The instance 'Stand02:3306' was successfully added to the cluster.
MySQL stand01:3306 ssl JS >
Add Node03 “Stand03”
MySQL stand01:3306 ssl JS > dr.addInstance('clust_admin@stand03:3306',{recoveryMethod: 'clone'})
Clone based recovery selected through the recoveryMethod option
Validating instance configuration at stand03:3306...
This instance reports its own address as Stand03:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'Stand03:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
A new instance will be added to the InnoDB Cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.
Adding instance to the cluster...
* Waiting for the Cluster to synchronize with the PRIMARY Cluster...
** Transactions replicated ############################################################ 100%
* Configuring ClusterSet managed replication channel...
** Changing replication source of Stand03:3306 to prim01:3306
Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.
* Waiting for clone to finish...
NOTE: Stand03:3306 is being cloned from stand02:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############################################################ 100% Completed
PAGE COPY ############################################################ 100% Completed
REDO COPY ############################################################ 100% Completed
NOTE: Stand03:3306 is shutting down...
* Waiting for server restart... ready
* Stand03:3306 has restarted, waiting for clone to finish...
** Stage RESTART: Completed
* Clone process has finished: 79.96 MB transferred in about 1 second (~79.96 MB/s)
State recovery already finished for 'Stand03:3306'
The instance 'Stand03:3306' was successfully added to the cluster.
MySQL stand01:3306 ssl JS >
DRCluster completes. Now on DR app host Bootstrap router AGAINST DRCluster.
[vagrant@drapp ~]$ sudo mkdir -p /mysqlrouter/
[vagrant@drapp ~]$ id mysqlrouter
uid=994(mysqlrouter) gid=990(mysqlrouter) groups=990(mysqlrouter)
[vagrant@drapp ~]$ sudo chown mysqlrouter:mysqlrouter /mysqlrouter/
[vagrant@drapp ~]$
[vagrant@drapp ~]$ sudo mysqlrouter --bootstrap clust_admin@stand01:3306 -d /mysqlrouter --account router_admin --user=mysqlrouter
Please enter MySQL password for clust_admin:
# Bootstrapping MySQL Router 8.4.9 (MySQL Community - GPL) instance at '/mysqlrouter'...
Please enter MySQL password for router_admin:
- Creating account(s) (only those that are needed, if any)
- Verifying account (using it to run SQL queries that would be run by Router)
- Storing account in keyring
- Adjusting permissions of generated files
- Creating configuration /mysqlrouter/mysqlrouter.conf
# MySQL Router configured for the InnoDB Cluster 'DRCluster'
After this MySQL Router has been started with the generated configuration
$ mysqlrouter -c /mysqlrouter/mysqlrouter.conf
InnoDB Cluster 'DRCluster' can be reached by connecting to:
## MySQL Classic protocol
- Read/Write Connections: localhost:6446
- Read/Only Connections: localhost:6447
- Read/Write Split Connections: localhost:6450
## MySQL X protocol
- Read/Write Connections: localhost:6448
- Read/Only Connections: localhost:6449
[vagrant@drapp ~]$ ls -ltr /mysqlrouter/
total 16
drwx------. 2 mysqlrouter mysqlrouter 6 May 27 19:19 run
-rw-------. 1 mysqlrouter mysqlrouter 83 May 27 19:19 mysqlrouter.key
-rwx------. 1 mysqlrouter mysqlrouter 146 May 27 19:19 stop.sh
-rwx------. 1 mysqlrouter mysqlrouter 294 May 27 19:19 start.sh
-rw-------. 1 mysqlrouter mysqlrouter 2308 May 27 19:19 mysqlrouter.conf
drwx------. 2 mysqlrouter mysqlrouter 29 May 27 19:19 log
drwx------. 2 mysqlrouter mysqlrouter 116 May 27 19:19 data
[vagrant@drapp ~]$
High availability is no longer optional for modern applications. Whether you’re running mission-critical workloads, customer-facing applications, or internal enterprise systems, database resilience and failover capability are essential.
In this walkthrough, we’ll build a 3-node MySQL 8.4 InnoDB Cluster with MySQL Router acting as the application connectivity layer. By the end, you’ll have a production-grade MySQL topology capable of automatic failover, read/write splitting, and simplified client connectivity.
In this we’ll cover:
Preparing MySQL nodes
Configuring InnoDB Cluster
Enabling Group Replication
Adding secondary nodes
Deploying MySQL Router
Exposing HA endpoints to applications
Architecture Overview
Our setup consists of:
Hostname
IP Address
Role
prim01
192.168.122.152
Primary Node
prim02
192.168.122.140
Secondary Node
prim03
192.168.122.139
Secondary Node
prodapp
192.168.122.155
MySQL Router Host
The deployment uses:
MySQL 8.4 LTS
Group Replication
Single-Primary topology
MySQL Shell
MySQL Router
Applications connect only to the Router layer, while Router intelligently forwards traffic to the correct backend node.
Step 1 — Prepare Storage Layout
On each database node, create dedicated directories for:
MySQL JS > dba.configureInstance('root@localhost:3306',{'clusterAdmin':'clust_admin'})
Configuring local MySQL instance listening at port 3306 for use in an InnoDB Cluster...
This instance reports its own address as prim01:3306
Clients and other cluster members will communicate with it through this address by default. If this is not correct, the report_host MySQL system variable should be changed.
Assuming full account name 'clust_admin'@'%' for clust_admin
Password for new account: ***********
Confirm password: ***********
applierWorkerThreads will be set to the default value of 4.
NOTE: Some configuration options need to be fixed:
Some variables need to be changed, but cannot be done dynamically on the server.
Do you want to perform the required configuration changes? [y/n]: y
Do you want to restart the instance after configuring it? [y/n]: y
Creating user clust_admin@%.
Account clust_admin@% was successfully created.
Configuring instance...
The instance 'prim01:3306' was configured to be used in an InnoDB Cluster.
Restarting MySQL...
NOTE: MySQL server at prim01:3306 was restarted.
MySQL JS >
Step 8 – Validate Instance Configuration
MySQL JS > dba.checkInstanceConfiguration('clust_admin@localhost:3306')
Please provide the password for 'clust_admin@localhost:3306': ***********
Save password for 'clust_admin@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): Y
Validating local MySQL instance listening at port 3306 for use in an InnoDB Cluster...
This instance reports its own address as prim01:3306
Clients and other cluster members will communicate with it through this address by default. If this is not correct, the report_host MySQL system variable should be changed.
Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected
Checking instance configuration...
Instance configuration is compatible with InnoDB cluster
The instance 'prim01:3306' is valid for InnoDB Cluster usage.
{
"status": "ok"
}
MySQL JS >
At this point, the instance is ready to participate in the cluster.
Step 9 – Repeat Configuration on All Nodes
Repeat Steps 1–8 on all the cluster nodes, in this case on prim02 and prim03
Step 10 – Create the InnoDB Cluster
Connect to the first node “prim01” using the cluster admin account and create an innodb cluster with the name “prod”
[vagrant@prim01 ~]$ mysqlsh --js
MySQL Shell 8.4.9
MySQL JS > \c clust_admin@localhost:3306
Creating a session to 'clust_admin@localhost:3306'
Fetching schema names for auto-completion... Press ^C to stop.
Your MySQL connection id is 12
Server version: 8.4.8 Source distribution
No default schema selected; type \use <schema> to set one.
MySQL localhost:3306 ssl JS >
MySQL localhost:3306 ssl JS > var cluster = dba.createCluster('prod')
A new InnoDB Cluster will be created on instance 'prim01:3306'.
Validating instance configuration at localhost:3306...
This instance reports its own address as prim01:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'prim01:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
Creating InnoDB Cluster 'prod' on 'prim01:3306'...
Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.
MySQL localhost:3306 ssl JS >
What Happens Internally?
When the cluster is created:
Group Replication is initialized
metadata schemas are created
recovery accounts are provisioned
replication channels are established
At this stage, only one node exists in the cluster.
Step 11 – Add Secondary Nodes
Add the second node:
MySQL localhost:3306 ssl JS > cluster.addInstance('clust_admin@prim02:3306')
NOTE: The target instance 'prim02:3306' has not been pre-provisioned (GTID set is empty). The Shell is unable to decide whether incremental state recovery can correctly provision it.
The safest and most convenient way to provision a new instance is through automatic clone provisioning, which will completely overwrite the state of 'prim02:3306' with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.
The incremental state recovery may be safely used if you are sure all updates ever executed in the cluster were done with GTIDs enabled, there are no purged transactions and the new instance contains the same GTID set as the cluster or a subset of it. To use this method by default, set the 'recoveryMethod' option to 'incremental'.
Please select a recovery method [C]lone/[I]ncremental recovery/[A]bort (default Clone): C
Validating instance configuration at prim02:3306...
This instance reports its own address as prim02:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'prim02:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
A new instance will be added to the InnoDB Cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.
Adding instance to the cluster...
Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.
* Waiting for clone to finish...
NOTE: prim02:3306 is being cloned from prim01:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############################################################ 100% Completed
PAGE COPY ############################################################ 100% Completed
REDO COPY ############################################################ 100% Completed
NOTE: prim02:3306 is shutting down...
* Waiting for server restart... ready
* prim02:3306 has restarted, waiting for clone to finish...
** Stage RESTART: Completed
* Clone process has finished: 74.31 MB transferred in about 1 second (~74.31 MB/s)
State recovery already finished for 'prim02:3306'
The instance 'prim02:3306' was successfully added to the cluster.
MySQL localhost:3306 ssl JS >
Then add the third node:
MySQL localhost:3306 ssl JS > cluster.addInstance('clust_admin@prim03:3306')
NOTE: The target instance 'prim03:3306' has not been pre-provisioned (GTID set is empty). The Shell is unable to decide whether incremental state recovery can correctly provision it.
The safest and most convenient way to provision a new instance is through automatic clone provisioning, which will completely overwrite the state of 'prim03:3306' with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.
The incremental state recovery may be safely used if you are sure all updates ever executed in the cluster were done with GTIDs enabled, there are no purged transactions and the new instance contains the same GTID set as the cluster or a subset of it. To use this method by default, set the 'recoveryMethod' option to 'incremental'.
Please select a recovery method [C]lone/[I]ncremental recovery/[A]bort (default Clone): C
WARNING: The instance 'prim03:3306' is only read compatible with the cluster, thus it will join the cluster in R/O mode.
Validating instance configuration at prim03:3306...
This instance reports its own address as prim03:3306
Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using 'prim03:3306'. Use the localAddress option to override.
* Checking connectivity and SSL configuration...
A new instance will be added to the InnoDB Cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.
Adding instance to the cluster...
Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.
* Waiting for clone to finish...
NOTE: prim03:3306 is being cloned from prim02:3306
** Stage DROP DATA: Completed
** Clone Transfer
FILE COPY ############################################################ 100% Completed
PAGE COPY ############################################################ 100% Completed
REDO COPY ############################################################ 100% Completed
NOTE: prim03:3306 is shutting down...
* Waiting for server restart... ready
* prim03:3306 has restarted, waiting for clone to finish...
** Stage RESTART: Completed
* Clone process has finished: 73.65 MB transferred in about 1 second (~73.65 MB/s)
State recovery already finished for 'prim03:3306'
The instance 'prim03:3306' was successfully added to the cluster.
MySQL localhost:3306 ssl JS >
Clone-Based Recovery
MySQL Shell recommends Clone Recovery for provisioning. This is one of the most powerful features in MySQL 8.x clustering.
Traditional replication setup often requires:
backups
restores
binlog positioning
manual synchronization
Clone recovery eliminates all of that.
The donor node physically transfers data files to the joining node automatically.
Step 12 – Verify Cluster Status
Retrieve cluster status:
MySQL localhost:3306 ssl JS > var cluster=dba.getCluster()
MySQL localhost:3306 ssl JS > cluster.status();
{
"clusterName": "prod",
"defaultReplicaSet": {
"name": "default",
"primary": "prim01:3306",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"prim01:3306": {
"address": "prim01:3306",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim02:3306": {
"address": "prim02:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.8"
},
"prim03:3306": {
"address": "prim03:3306",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.4.9"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "prim01:3306"
}
MySQL localhost:3306 ssl JS >
This confirms cluster is up and healthy.
Step 13 – Deploying MySQL Router
Without MySQL Router:
applications must track failovers
clients need topology awareness
connection strings become complex
Router abstracts all cluster intelligence away from applications.
Step 14 – Install MySQL Router
On the application host install the router binaries.
[FATAL] [DBAAS-70289] Cluster is not in NORMAL or FORCED state. Current detected state is: ‘ROLLING PATCH’. ACTION: Cluster should be in NORMAL or FORCED state to perform the current operation.
[FATAL] [DBAAS-70289] Cluster is not in NORMAL or FORCED state.
Current detected state is: 'ROLLING PATCH'.
ACTION: Cluster should be in NORMAL or FORCED state to perform the current operation.
This issue occurs when Oracle Clusterware still believes the cluster is in a rolling patching state, even though patching activities may already be completed successfully.
In this article, I’ll explain how I verified the cluster state and fixed the issue on both RAC nodes.
Environment
Oracle RAC 19c
2-Node Cluster
Grid Infrastructure Version: 19.0.0.0.0
Problem Symptoms
The cluster state was showing:
The cluster upgrade state is [ROLLING PATCH]
This prevented further DBAAS or cluster-related operations from completing successfully.
Step 1: Verify Clusterware Release Version
Run the following command on both nodes as the grid user.
Node01
[grid@drvm01 ~]$ crsctl query crs releaseversion
Oracle High Availability Services release version on the local node is [19.0.0.0.0]
Node02
[grid@drvm02 ~]$ crsctl query crs releaseversion
Oracle High Availability Services release version on the local node is [19.0.0.0.0]
In Oracle 19c multitenant environments, you may notice an interesting behavior while taking RMAN backups with:
CONFIGURE BACKUP OPTIMIZATION ON;
When backup optimization is enabled, RMAN often skips backing up the PDB$SEED datafiles if they were already backed up previously. However, when backup optimization is disabled or when using the FORCE option, RMAN backs up all datafiles including PDB$SEED.
This behavior is expected and is part of RMAN’s optimization logic. In this article, we will explore:
What RMAN backup optimization does
Why PDB$SEED gets skipped
How BACKUP FORCE changes the behavior
Practical examples
Important considerations for DBAs
What Is RMAN Backup Optimization?
RMAN backup optimization is a feature that prevents Oracle from backing up files that already have a sufficient backup according to the configured retention policy.
It can be enabled using:
RMAN> CONFIGURE BACKUP OPTIMIZATION ON;
When enabled, RMAN checks whether a datafile, archived redo log, or backup set already exists and satisfies the backup requirements. If yes, RMAN skips it to save:
Backup time
Storage space
Network bandwidth
This is especially useful in large environments with many unchanged files.
Understanding the Role of PDB$SEED
In a multitenant database architecture, PDB$SEED is a template pluggable database used for creating new PDBs.
Characteristics of PDB$SEED:
Read-only by default
Rarely changes
Shared template for PDB creation
Contains system metadata and seed structures
Since the seed database remains mostly unchanged, its datafiles often remain identical across backups.
Why RMAN Skips PDB$SEED Datafiles
Suppose you run a backup like this:
BACKUP DATABASE PLUS ARCHIVELOG;
with backup optimization enabled.
RMAN evaluates each datafile and determines whether it already has a valid backup. Since PDB$SEED datafiles typically do not change, RMAN identifies them as already backed up and skips them.
You may see messages similar to:
skipping datafile 2; already backed up 2 time(s)
skipping datafile 4; already backed up 2 time(s)
skipping datafile 6; already backed up 2 time(s)
These datafiles usually belong to PDB$SEED. This behavior is completely normal.
Managing Oracle patch storage is an important maintenance activity for database administrators. Over time, Oracle homes accumulate backup files, patch artifacts, and unnecessary inventory data that can consume significant disk space.
Oracle provides the OPatch cleanup utility to safely remove obsolete patch backup files and reclaim storage without impacting the current Oracle installation.
This blog explains how to clean up Oracle patch storage step by step using the OPatch utility.
Why Clean Oracle Patch Storage?
Frequent patching activities create backup copies and metadata under the Oracle Home. Over time, this can lead to:
Increased Oracle Home size
Filesystem space exhaustion
Slower patch management operations
Inventory clutter
Regular cleanup helps maintain a healthy Oracle environment and optimizes storage utilization.
Prerequisites
Before performing cleanup:
Ensure you have a valid backup of the Oracle Home.
Oracle 12c introduced one of the most useful Data Guard recovery features: RMAN active restore from service. Instead of manually copying backup pieces, transferring datafiles, or using duplicate commands, you can rebuild a standby database directly across the network from the primary database using RMAN.
This approach is especially useful when:
The standby database is corrupted
Datafiles are missing
Storage migration is required
ASM disk groups changed
You want to recreate the standby quickly without backup shipping
In this article, we will walk through a complete example using the following RMAN commands:
Data security is no longer optional. Whether you’re running MySQL in production, development, or a cloud-native environment, protecting sensitive data at rest is a critical requirement.
One of the most effective ways to secure stored data in MySQL is by enabling Transparent Data Encryption (TDE).
In this guide, we’ll walk through how to enable TDE in MySQL 8.0 using the keyring_file plugin on both Windows and Linux platforms. We’ll also demonstrate how to encrypt tables and databases, verify encryption status, and convert existing objects to use TDE.
What is TDE in MySQL?
Transparent Data Encryption (TDE) encrypts your InnoDB tablespaces automatically, protecting data stored on disk without requiring application changes.
With TDE enabled, MySQL encrypts:
Table data
Tablespaces
Associated storage files
The encryption and decryption happen transparently while MySQL is running.
TDE Plugin Differences Between Windows and Linux
The implementation steps are nearly identical on both operating systems. The only difference is the plugin library file used during startup.
Platform
Plugin
Windows
keyring_file.dll
Linux
keyring_file.so
Although this walkthrough uses a Windows example, the same approach applies to Linux with the appropriate plugin filename.
Step 1 – Configure MySQL for TDE
To enable TDE, configure the keyring_file plugin inside your MySQL configuration file.
Starting from MySQL 8.0.16, encryption can also be enabled at the database level. This allows all newly created tables inside the database to inherit encryption automatically.
Step 9 – Create an Encrypted Database
CREATE DATABASE db1 ENCRYPTION='Y';
USE db1;
CREATE TABLE test (
id INT,
secret TEXT
);
INSERT INTO test VALUES (301,'Hello');
Verify encryption:
SELECT TABLE_SCHEMA,
TABLE_NAME,
CREATE_OPTIONS
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')
AND CREATE_OPTIONS LIKE '%ENCRYPTION%';
+--------------+------------+----------------+
| TABLE_SCHEMA | TABLE_NAME | CREATE_OPTIONS |
+--------------+------------+----------------+
| db1 | test | ENCRYPTION='Y' |
+--------------+------------+----------------+
The table inherited encryption automatically from the database.
Step 10 – Alter an Existing Database for TDE
Now let’s see what happens when encryption is enabled on an existing database.
Create a normal database first, and in it create an unencrypted table
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE testtab (
id INT,
secret TEXT
);
At this point, database as well as the table is not encrypted.
Enable Encryption on the Database
ALTER DATABASE testdb ENCRYPTION='Y';
Now create another table:
CREATE TABLE testtab2 (
id INT,
secret TEXT
);
Verify encryption settings:
SELECT TABLE_SCHEMA,
TABLE_NAME,
CREATE_OPTIONS
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')