Oracle

Software Mile works with Oracle databases and the applications built around them – the systems many enterprises still run their core operations on, and which integration projects have to respect rather than route around.

Working With Oracle in the Real World

  • Application development against Oracle databases, with PL/SQL where it belongs
  • Integration between Oracle systems and modern web, mobile, and cloud applications
  • Performance work on queries and data access that have grown slow over years
  • Migration and modernization planning where Oracle is being moved or wrapped

Oracle is often the system of record everything else must talk to. Tell us your Oracle environment and what needs to connect to it.

Should You Migrate Off Oracle, or Fix What You Have?

Licensing cost is the usual reason the question gets asked, and it is a legitimate one. The cost of leaving, though, rarely sits in the database itself. It sits in the application: stored procedure logic, Oracle-specific SQL, sequences and hierarchical queries, scheduled jobs, and every report written against the schema over the years. How much of that exists, and how much of it is still in use, is what separates a contained migration from an open-ended one.

Sometimes the honest recommendation is to stay and address what actually hurts: a handful of slow reports, an unsupported version, or an application layer nobody can change. Before committing either way, confirm your licensing position with Oracle or your reseller. Internal assumptions about entitlement have a way of aging quietly.

How Much Logic Belongs in PL/SQL?

Set-based data work belongs close to the data. Pulling a million rows into application memory to filter them is slower and more fragile than a well-written query, and PL/SQL exists for exactly that. Business workflow is a different matter: rules that change with the business are easier to test, version, and hand to a new developer when they live in application code.

The failure mode worth guarding against is a system where nobody can say where a rule is implemented, because it exists in a trigger, a procedure, and the application at once. Deciding the boundary and writing it down is worth more than any particular choice of where to draw it.

Why Is It Slow, and How Would You Find Out?

Start with measurement. Before adding indexes or hardware, get the execution plan and the actual wait profile for the statement people are complaining about, at the data volumes they are complaining about. The answer is frequently somewhere nobody expected, and sometimes outside the database entirely.

  • Stale statistics. The optimizer chooses badly when it is working from an old picture of the data.
  • Application-side N+1 queries. An ORM issuing thousands of small statements looks like a database problem and is not one.
  • Literals instead of bind variables. Every query becomes a new parse, and the shared pool works harder than the disks do.
  • Indexes added for a problem you no longer have. They still cost on every write, and some are no longer used by anything at all.
  • Growth nobody planned for. A query that was fine on a small table can behave completely differently once it has grown by orders of magnitude, and the answer may be partitioning or archiving.

Oracle the Database, or Oracle the Applications?

The name covers very different products, and which one you mean changes what kind of help you need. The scope of this page is the Oracle database, the custom applications built on it, and integration with packaged Oracle systems from the outside. Configuring or extending E-Business Suite or Fusion applications internally is a separate specialty and is not covered here.

If the question is how to connect an Oracle-backed system to a web or mobile front end, a partner, or a cloud service, that is the work described here and on the REST APIs page.

Connecting Oracle to Modern Applications

Giving each new application its own database account and letting it write directly is the pattern that ages worst. Direct schema access turns every future change to the database into a negotiation with every consumer, and it puts data integrity in the hands of whoever wrote the most recent client.

An API layer in front of the database gives you one place to enforce validation, one place to change when the schema evolves, and something you can log, monitor, and rate limit. It also narrows the blast radius of an eventual database migration to a single component.

Edition, Version, and Features That Are On By Default

Version matters less than people expect for application work and more than expected for support. 18c and 19c are patch sets of the 12.2 family, 12.2.0.2 and 12.2.0.3, which is why moving 12.2 to 19c is usually undramatic while moving 11.2 to 19c is a different exercise. 21c was an innovation release with a short window; 23ai is the current long term release, and the current error correction date for 19c sits in Oracle's Lifetime Support Policy document, which has been revised more than once. One part of an upgrade is not reversible in place: raising the COMPATIBLE parameter enables on-disk format changes, and lowering it again means restoring a backup.

Edition and options are two different things, and only one of them is visible in the software. Standard Edition 2 and Enterprise Edition are separate products, capped differently in hardware and missing whole features, and no setting turns those features on. Options and management packs behave the opposite way. They ship inside the Enterprise Edition binaries and are enabled by default, so partitioning a table, running a SQL Tuning Advisor task, or pulling an AWR report all work regardless of what was purchased. DBA_FEATURE_USAGE_STATISTICS records that they worked.

  • Standard Edition 2 is licensed on servers of at most two sockets, and the database uses a maximum of 16 CPU threads no matter how many the machine has. Past that point, a bigger box does not make an SE2 database faster.
  • Not present in SE2 at all: partitioning, parallel query and parallel DML, bitmap indexes, materialized view query rewrite, Flashback Database, online index rebuild, and, since 19c, RAC. A data model that depends on partitioning is an Enterprise Edition data model, and that is a licensing decision made at design time rather than a tuning decision made later.
  • Separately licensed Enterprise Edition options include Partitioning, Advanced Compression, Advanced Security and its transparent data encryption, Database In-Memory, Active Data Guard, Real Application Testing, and Multitenant beyond the pluggable databases the base license already covers.
  • Diagnostics Pack covers AWR, ADDM, active session history and the DBA_HIST_* views. Tuning Pack covers SQL Tuning Advisor, SQL Access Advisor, and Real-Time SQL Monitoring. A single SELECT against V$ACTIVE_SESSION_HISTORY is a Diagnostics Pack use, which is why pack questions come up during troubleshooting rather than during procurement.
  • CONTROL_MANAGEMENT_PACK_ACCESS governs all of it, with values NONE, DIAGNOSTIC, and DIAGNOSTIC+TUNING. The default is DIAGNOSTIC+TUNING, so the packs are live unless somebody set the parameter. Statspack is the pack-free alternative and still ships with the database.
  • Autonomous Database and the Enterprise Edition cloud tiers include the management packs in the service. A diagnostic routine that is entirely normal there is not automatically appropriate on the on-premises instance it was copied from.

Getting the Plan That Actually Ran

The plan that matters is the one in the cursor cache, not the one EXPLAIN PLAN prints. EXPLAIN PLAN, and the plan button in most tools, re-optimize the statement without peeking at real bind values, so they can show a plan the database never chose. Find the SQL_ID in V$SQL, then run DBMS_XPLAN.DISPLAY_CURSOR against it. When you can execute the statement yourself, add the GATHER_PLAN_STATISTICS hint and the ALLSTATS LAST format, which prints estimated and actual rows for every row source. The first line where those two diverge by an order of magnitude is usually where the problem starts, and everything below it is consequence.

Separate the questions before choosing a tool. One statement being slow is answered per statement, from the plan and the row source timings. The instance being slow is answered from the instance-wide wait profile and usually names something the statement had no control over. There is a third case neither tool shows: the database reports forty milliseconds for a statement the user experiences as four seconds, and the gap is round trips and fetch size on the client. Comparing the client's own timing against the database's own timing is what tells the three apart.

  • V$SESSION.EVENT and STATE say what a session is waiting on right now. Active session history, sampled once per second into V$ACTIVE_SESSION_HISTORY, says what it was waiting on ten minutes ago, which is the question you actually have once somebody complains. It is Diagnostics Pack.
  • Statspack installs from rdbms/admin/spcreate.sql and needs no pack license. It takes instance-level snapshots and reports the delta between two of them. It has no per-session history, so it can tell you the database was busy and cannot tell you which session waited on what.
  • Real-Time SQL Monitoring, through DBMS_SQLTUNE.REPORT_SQL_MONITOR, captures statements automatically once they pass a few seconds of CPU or IO or go parallel, and shows elapsed time per plan line while the statement is still running. It requires the Tuning Pack.
  • "It got slow overnight and nothing changed" is usually a plan change. Compare PLAN_HASH_VALUE for the SQL_ID over time. More than one child cursor for the same statement means the database decided the cursors could not be shared, and V$SQL_SHARED_CURSOR has a column set to Y giving the reason.
  • ORA-01555, snapshot too old, is a query duration problem rather than a connection or memory problem. A long-running query needs a consistent read of blocks that concurrent DML has since changed, and the undo holding the older version has been overwritten. The levers are undo retention and the size of the undo tablespace, or making the query finish sooner; rerunning it unchanged on a busier system just fails earlier.
  • Gathering statistics invalidates cursors and can change plans, which makes it a fix and a risk in the same action. DBMS_STATS.SET_TABLE_PREFS with PUBLISH set to FALSE lets you gather into pending statistics, test them in one session with OPTIMIZER_USE_PENDING_STATISTICS, and publish only after the plans are known.

Drivers, Character Sets, and Types That Do Not Map Cleanly

Most current languages offer both a thin driver that speaks the Oracle network protocol directly and a thick mode that loads Oracle Instant Client: JDBC thin, managed ODP.NET, python-oracledb, node-oracledb. Thin is the default worth starting from. There is no native library in the container image, no Instant Client version to keep matched to anything, and the driver performs its own character set conversion rather than depending on NLS_LANG being set correctly in the environment. Thick mode is for the features thin does not implement.

What thin leaves out shrinks with each release, so the driver's own feature matrix is the authority rather than anything written a year ago. The recurring reasons to fall back to thick mode are Advanced Queuing, continuous query notification, and external authentication such as Kerberos. Deciding this early is worth the fifteen minutes, because the decision shows up in the base image, the build, and the deployment, not in application code.

  • Oracle stores an empty string in a VARCHAR2 as NULL. A driver that binds an empty string writes NULL, the row reads back NULL, and a NOT NULL constraint rejects a value the application thought it had supplied.
  • Check NLS_CHARACTERSET before assuming column widths. Under AL32UTF8 a character can occupy up to four bytes, and VARCHAR2(30) means 30 bytes unless the column was declared VARCHAR2(30 CHAR) or NLS_LENGTH_SEMANTICS is CHAR. A 30-character name that was fine on a single-byte database fails with ORA-12899 after the character set changes.
  • DATE carries a time to the second, with no fractional seconds and no time zone. TIMESTAMP WITH LOCAL TIME ZONE converts on the way in and out using the session time zone, so the same stored row reads back differently for two sessions in different zones, which is either exactly what you wanted or a bug that only appears in one office.
  • NUMBER is decimal, not binary floating point. Mapping it to a double in the client loses precision on money. Java maps it to BigDecimal correctly and then the JSON serializer turns it into a float, which is where the rounding usually enters.
  • Every fetch is a round trip. JDBC defaults to a fetch size of 10 rows and python-oracledb to an array size of 100, so ten thousand rows across a link with thirty milliseconds of latency is dominated by round trips, not by the query. The database's own timing for that statement looks fine, because it is fine.
  • The connection errors distinguish themselves. ORA-12154 means the client could not resolve the connect identifier, so it is a tnsnames or EZConnect problem before the network is involved. ORA-12541 means nothing is listening on the port. ORA-12514 means the listener answered but does not know that service name. ORA-12170 is a timeout, which is usually a firewall dropping the connection attempt rather than a database that is down.

Connection Pools, Process Limits, and Sessions That Vanish

Oracle's default dedicated server model gives every session its own operating system process, and PROCESSES caps how many exist. When the instance runs out, ORA-00020 is raised for whoever asks next, which is frequently not the application that consumed them. That makes pool sizing an instance-wide concern rather than a per-application one, and it is worth writing the arithmetic down somewhere both sides can see it.

Sessions also disappear quietly. A pooled connection sits idle longer than a firewall's idle timeout, the firewall drops the state without telling either end, and the next borrow from the pool fails with ORA-03113, end-of-file on communication channel, or ORA-03135, connection lost contact. The database never noticed. The client-side fix is a validation query or the driver's built-in ping on borrow; the server-side fix is SQLNET.EXPIRE_TIME so the instance detects dead clients and releases their processes instead of holding them against the limit.

  • Add up maximums, not averages. Every pool's maximum size, plus batch jobs, plus reporting tools and ad hoc sessions, plus background processes, has to fit under PROCESSES. Pools configured to grow under load all grow during the same five minutes.
  • A pool larger than the database's CPU count usually lowers throughput. Queuing at the pool is visible and bounded; the same queuing pushed into the database appears as latch and buffer contention, which looks like an entirely different problem.
  • Whatever does connect, the API tier, batch jobs, reporting tools, should connect under its own credentials, and the driver should set MODULE and ACTION through DBMS_APPLICATION_INFO or its own client-info calls. V$SESSION and the session history then say which caller a session belongs to. Thirty identical sessions under one shared username on one host are unattributable at three in the morning, when exactly one of them is the problem.
  • A resource profile with IDLE_TIME snipes idle sessions. The session sits in V$SESSION marked SNIPED and the client eventually receives ORA-02396. A pool that holds connections open through a quiet night meets this every morning.
  • Connect on a named application service rather than the SID or the default service. Resource Manager, tracing through DBMS_MONITOR, and later relocation of that workload all key off the service name, and none of the connect strings have to change when the database moves.
  • Database Resident Connection Pooling is the answer when many short-lived client processes each want a connection, such as serverless functions or a per-request process model. DBMS_CONNECTION_POOL.START_POOL plus SERVER=POOLED in the connect descriptor puts the pool inside the database rather than one pool per client process.

Semantics That Change When the Data Moves

Application logic is the visible half of a migration. Underneath it sits a set of behaviors that change silently, because the new database accepts the same statement and returns a different answer. Nothing raises an error, so a test suite written against the old database passes unless the test data happens to contain the edge case.

The way to find these before users do is a dual run: point both databases at the same inputs, run the same workload, and compare outputs row by row instead of confirming that both finished. Differences cluster around NULLs, dates, numeric rounding, and sort order, which is also a reasonable order to check them in.

  • Oracle stores an empty string as NULL; Postgres does not. After a migration a column can hold both, so IS NULL checks that used to be sufficient now miss rows, and a NOT NULL constraint stops catching the case it was added for.
  • An Oracle DATE has a time component. Mapping it to a Postgres or SQL Server DATE truncates that silently, and it surfaces later as a BETWEEN filter that drops the last day or a sort that no longer breaks ties.
  • Oracle's default sort is binary, so upper case sorts ahead of lower case. Postgres sorts by the database's collation, which in most locales does not. Every sorted screen changes, and any pagination that assumed a stable ORDER BY changes with it.
  • DDL commits the open transaction in Oracle and is transactional in Postgres. Deployment and data-fix scripts written on the Oracle assumption behave differently, usually more safely, but anything that relied on an implicit commit partway through now succeeds or rolls back as one unit.
  • Autonomous transactions, PRAGMA AUTONOMOUS_TRANSACTION, are typically used for logging that has to survive a rollback. There is no direct equivalent in Postgres, so each one becomes a separate connection or an out-of-band log, and the choice has to be made per use rather than once.
  • Sequences with CACHE leave gaps after an instance restart, and NOORDER on RAC interleaves values across nodes. Code that treated the sequence as a gapless counter was already wrong; the migration is simply when someone finally reads the numbers.

Frequently Asked Questions

What has to exist before integration work can start?

A reachable instance, an account with the grants the work needs, a network path to the listener, and an environment that is not production. The listener is on 1521 by default, with 2484 the usual choice for TCPS and 1522 for Autonomous Database. Beyond the access, you need somebody who can say what the tables mean. Column names and constraints describe storage, not intent, and nothing in the data dictionary distinguishes a status code that is dead from one that half the business depends on.

What does connecting to Autonomous Database require that an on-premises instance does not?

TLS, and a wallet if mutual TLS is in use. The wallet downloads as a zip containing tnsnames.ora, sqlnet.ora, and the certificate store, the connection uses port 1522, and the aliases in that tnsnames file are what the application connects on. One-way TLS is available as an alternative and removes the wallet in exchange for maintaining an access control list of source addresses. Wallets expire, so whoever owns the deployment needs to know that rotating one is a scheduled task rather than a one-time setup step.

What do you need from us to diagnose a slow statement?

Read access and a way to reach the statement. Concretely that means SELECT_CATALOG_ROLE or equivalent grants on the V$ and DBA_HIST views, ALTER SESSION so tracing can be turned on, and the SQL_ID or the statement text with representative bind values. No write access is required and no copy of the data has to leave your environment. If the Diagnostics or Tuning Pack is not licensed on that instance, say so at the start, because it changes which tools are usable rather than whether the work can be done.

Do schema changes require an outage?

Usually not, but DDL takes a brief exclusive lock on the object and fails immediately with ORA-00054 if a transaction is holding it. Setting DDL_LOCK_TIMEOUT makes the session wait for the lock instead of failing on the first attempt, which is what makes a change deployable during business hours. Adding a column with a default has been metadata-only since 11g for NOT NULL columns and since 12c for nullable ones, so it does not rewrite the table. Index builds and table redefinition that do not block writes use ONLINE, which is Enterprise Edition.

Does every consumer have to go through the API layer?

No. A read-only consumer can be given a schema of views with SELECT granted and nothing else, and those views become the contract while the physical tables stay free to change behind them. For a reporting tool that already speaks SQL, that is lighter than an API and keeps the property that matters, which is that no consumer is bound to the tables themselves. Serving those reads from a physical standby is a separate decision, since opening a standby for queries is Active Data Guard.

Does the database have to be upgraded before any of this?

For integration work, usually not. The requirement is a supported driver reaching a supported server, and Oracle's client and server interoperability matrix decides which pairs qualify rather than the version numbers looking close to each other. An unsupported database version is a separate matter: it keeps running, but a bug you hit has no patch coming, which changes what a safe response to a production problem looks like.

What does the customer side actually have to decide?

Which system is the source of truth for each entity that appears in more than one place, and what the interface is allowed to expose. After that, who owns schema changes, and whether the new layer may require a change to the schema or has to work with what is already there. These are not technical questions and they cannot be read off the schema, but every technical decision downstream depends on them.