<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Rick Houlihan</title>
    <description>The latest articles on Forem by Rick Houlihan (@rick_houlihan_cf110dba340).</description>
    <link>https://forem.com/rick_houlihan_cf110dba340</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3876871%2F0e994cec-79ad-4121-be38-dd33655ef9d0.jpg</url>
      <title>Forem: Rick Houlihan</title>
      <link>https://forem.com/rick_houlihan_cf110dba340</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rick_houlihan_cf110dba340"/>
    <language>en</language>
    <item>
      <title>Lakebase, Meet PDB: The "Third-Generation" Database Oracle Shipped in 2013</title>
      <dc:creator>Rick Houlihan</dc:creator>
      <pubDate>Mon, 11 May 2026 19:32:27 +0000</pubDate>
      <link>https://forem.com/rick_houlihan_cf110dba340/lakebase-meet-pdb-the-third-generation-database-oracle-shipped-in-2013-4l8b</link>
      <guid>https://forem.com/rick_houlihan_cf110dba340/lakebase-meet-pdb-the-third-generation-database-oracle-shipped-in-2013-4l8b</guid>
      <description>&lt;p&gt;&lt;strong&gt;By Rick Houlihan &amp;amp; Patrick Meredith&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Databricks named the right problem. Their answer is a credible execution of an idea Oracle Multitenant solved a decade earlier — and as it turns out, the gap they think they've found in Oracle was only one PL/SQL package away from closing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Pitch That Started This
&lt;/h2&gt;

&lt;p&gt;A colleague forwarded me the Databricks blog post the other day. Opening line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"In our previous blog, we introduced Lakebase, the third-generation database architecture that fundamentally separates storage and compute."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— Databricks, &lt;em&gt;"How agentic software development will change databases"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, like what Oracle did 12 years ago.&lt;/p&gt;

&lt;p&gt;I'm being a little snide. Bear with me — there's a real article underneath. The blog is a thoughtful read about how AI agents are changing database workloads, and most of the diagnosis is right. Their telemetry is interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"In Databricks's Lakebase service, AI agents now create roughly 4x more databases than human users."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"[O]n average, each database project has ~10 branches and some databases with nested branches reaching depths of over 500 iterations…"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"[F]or about half of these agentic applications, the database compute lifetime is less than 10 seconds."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number is real. Agents don't behave like humans. They generate variants by the dozen, run them in parallel, evaluate against an eval set, keep the winner, throw away the losers. Evolutionary development. The economics break down completely on a database that costs $200/month per instance with a five-minute provisioning cycle.&lt;/p&gt;

&lt;p&gt;So Databricks is right about the problem. They're right that databases need a branching primitive. They're right that storage and compute need to scale independently. They're right that the always-on cost floor doesn't survive contact with agents.&lt;/p&gt;

&lt;p&gt;This article is not about whether they're wrong on the diagnosis.&lt;/p&gt;

&lt;p&gt;It's about whether &lt;strong&gt;their answer is novel&lt;/strong&gt; — and what the architecture-correct version looks like. Because Oracle has been shipping the same primitive in the engine since July 2013, and a small Python + PL/SQL wrapper is all that separates it from the developer experience Databricks just announced.&lt;/p&gt;

&lt;p&gt;Patrick and I thought it was worth writing this down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Lakebase Actually Is
&lt;/h2&gt;

&lt;p&gt;Spoiler: it's Neon.&lt;/p&gt;

&lt;p&gt;Databricks announced its agreement to acquire Neon on May 14, 2025. The press release didn't disclose a price (industry reporting put it at roughly $1 billion), but it did volunteer a useful telemetry data point: &lt;em&gt;"over 80 percent of the databases provisioned on Neon were created automatically by AI agents rather than by humans."&lt;/em&gt; That number is also the reason this acquisition happened — Neon, founded in 2021 by Postgres committers, had built a serverless Postgres architecture that AI agents could actually afford to use: stateless compute nodes, a Paxos-based safekeeper quorum holding WAL, and a pageserver materializing pages on demand from object storage. Branches were stamped as metadata pointers at a moment in WAL history; copy-on-write at the storage layer made divergence cheap.&lt;/p&gt;

&lt;p&gt;That architecture is good engineering. It's also exactly what Databricks now ships as Lakebase. Their own architecture deep-dive opens with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"In the lakebase architecture, your compute is stateless. It does not rely on a local data directory. Instead, it streams WAL to a Paxos-based quorum of safekeepers."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— Databricks, &lt;em&gt;"How lakebase architecture delivers 5x faster Postgres writes"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same post describes how, when Postgres compute requests a page from storage, the pageserver &lt;em&gt;"reconstructs it by finding the most recent materialized image of that page and replaying any WAL deltas on top."&lt;/em&gt; If you've read Neon's published architecture overview, this is familiar vocabulary — stateless compute → safekeepers → pageserver → object storage — because it &lt;em&gt;is&lt;/em&gt; Neon's architecture. Lakebase is Neon with a Databricks brand on top.&lt;/p&gt;

&lt;p&gt;To be clear: that's not a problem. Neon is good engineering. Acquiring it and integrating it with the lakehouse is a perfectly defensible product move — buying a four-year-old startup whose technology already solves the agent-economics problem is faster than building one yourself. Nobody should be mad about an acquisition.&lt;/p&gt;

&lt;p&gt;The problem is the next thing Databricks did, which was call a four-year-old Postgres-branching architecture &lt;em&gt;"the third-generation database architecture that fundamentally separates storage and compute."&lt;/em&gt; That's a marketing claim, not an architectural one, and it has two specific issues. First, "third generation" implies a chronology — first generation was monolithic, second was something, this is the third — and Databricks has never been particularly clear about what the second generation was, which is convenient because any honest answer would include systems older than Lakebase that already do what Lakebase does. Second, the &lt;em&gt;"fundamentally separates storage and compute"&lt;/em&gt; phrasing treats compute/storage separation as a 2025 innovation, which is awkward when Snowflake shipped that architecture commercially in 2014 and Oracle shipped a multitenant variant of it in July 2013.&lt;/p&gt;

&lt;p&gt;"Third generation" sells better than "we acquired a 2021 startup six months ago, here's what they built." It also doesn't survive a history check.&lt;/p&gt;

&lt;p&gt;That's the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Third-Generation" Sleight of Hand
&lt;/h2&gt;

&lt;p&gt;Same Databricks blog post — &lt;em&gt;"A New Era of Databases: Lakebase,"&lt;/em&gt; June 12, 2025 — one "Database Architecture Evolution" section, three generations laid out in sequence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generation 1 — the monoliths:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Examples: MySQL, Postgres, classic Oracle"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Database systems started as absolute monoliths."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Generation 2 — proprietary loose coupling:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Examples: Aurora, Oracle Exadata"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"As cloud infrastructure improved, vendors physically separated storage from compute, moving storage into proprietary backend tiers."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same Oracle. Two generations. One page apart. Pick one.&lt;/p&gt;

&lt;p&gt;I'll be charitable and assume the intended argument was &lt;em&gt;"early Oracle was a monolith, modern Oracle isn't."&lt;/em&gt; Fine. Then "modern" deserves a timeline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;What was separated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oracle Real Application Clusters (RAC)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple compute nodes against a single shared SAN/NAS storage substrate (Oracle 9i)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oracle Exadata v1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Database servers vs. intelligent storage cells with predicate offload (Smart Scan), GA September 2008&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google Dremel / BigQuery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Disaggregated storage and compute, columnar — VLDB 2010 paper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;July 1, 2013&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oracle Database 12c / Multitenant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;CREATE PLUGGABLE DATABASE … FROM … SNAPSHOT COPY&lt;/code&gt; ships in the engine&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Snowflake (GA)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three-layer cloud-native: storage / virtual warehouses / cloud services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nov 2014 / Jul 2015&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Amazon Aurora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Compute decoupled from a 6-way replicated storage layer across 3 AZs (preview Nov 2014, GA July 2015)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Neon (founded)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Postgres-specific WAL-level disaggregation with branching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;May 14, 2025&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Lakebase&lt;/strong&gt; = Databricks acquires Neon&lt;/td&gt;
&lt;td&gt;Neon's architecture wrapped around open lake storage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftn7q2ruc2ujku70azhz2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftn7q2ruc2ujku70azhz2.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Storage and compute have been separated in production databases for 25 years. Across two paradigms, four vendors, and at minimum seven shipping systems before Lakebase showed up. "Third generation" isn't an architectural claim. It's a marketing label that requires the reader to forget about Oracle RAC, Exadata, Dremel, Multitenant, Snowflake, Aurora, and Neon in roughly that order.&lt;/p&gt;

&lt;p&gt;So what's actually new in Lakebase? The same blog is honest about this if you read past the generation label:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Like Gen 2, it separates compute from storage, but with a critical difference: both the storage infrastructure and the data formats are completely open."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translation: Gen 2 already separated storage from compute. Their own text concedes the point. The Gen 3 differentiator they're actually claiming is &lt;em&gt;open data formats&lt;/em&gt;. We'll dismantle that claim in Section 10 — short version, "open formats" turns out to do less work than the marketing suggests once you ask which formats, governed by whom, queryable how. But file the claim for now.&lt;/p&gt;

&lt;p&gt;The other thing the launch blog flags as Gen 3 distinctive is branching:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Databases can be branched and cloned the way developers branch code."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Branching as a developer-experience primitive is a fair thing to call out — it genuinely changes how AI agents and dev workflows interact with databases, and we conceded that point in Section 1. Branching as a &lt;em&gt;database-engine&lt;/em&gt; primitive, though, has shipped in Oracle Multitenant since July 1, 2013, with documented syntax, multiple supported storage substrates, and a hard limit four to eight times higher than Lakebase's. Which is the next section.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Third-generation database architecture? We're on our fifth." - Patrick Meredith&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PDB Snapshot Copy: The Branching Primitive Oracle Has Shipped Since 2013
&lt;/h2&gt;

&lt;p&gt;The syntax is one statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;my_experiment_branch&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;base_experiment_pdb&lt;/span&gt;
  &lt;span class="n"&gt;SNAPSHOT&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Oracle 19c SQL Reference describes what happens underneath:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The &lt;code&gt;SNAPSHOT COPY&lt;/code&gt; clause instructs the database to clone the source PDB using storage snapshots. This reduces the time required to create the clone because the database does not need to make a complete copy of the source data files."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-PLUGGABLE-DATABASE.html" rel="noopener noreferrer"&gt;Oracle Database 19c SQL Language Reference: CREATE PLUGGABLE DATABASE&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What "storage snapshots" means depends on the substrate. The same reference is explicit: with &lt;code&gt;CLONEDB=FALSE&lt;/code&gt;, &lt;em&gt;"the underlying file system for the source PDB's files must support storage snapshots. Such file systems include Oracle Automatic Storage Management Cluster File System (Oracle ACFS) and Direct NFS Client storage."&lt;/em&gt; With &lt;code&gt;CLONEDB=TRUE&lt;/code&gt;, &lt;em&gt;"the underlying file system for the source PDB's files can be any local file system, network file system (NFS), or clustered file system that has Direct NFS enabled. However, the source PDB must remain in open read-only mode as long as any clones exist."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage substrate&lt;/th&gt;
&lt;th&gt;Snapshot mechanism&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oracle ACFS&lt;/td&gt;
&lt;td&gt;Copy-on-write storage snapshots&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLONEDB=FALSE&lt;/code&gt; path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct NFS Client (dNFS)&lt;/td&gt;
&lt;td&gt;Copy-on-write storage snapshots on snapshot-capable NFS array&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CLONEDB=FALSE&lt;/code&gt; path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exadata sparse disk groups&lt;/td&gt;
&lt;td&gt;Copy-on-write&lt;/td&gt;
&lt;td&gt;Source PDB must be read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard FS + &lt;code&gt;CLONEDB=TRUE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;dNFS sparse files over NFS&lt;/td&gt;
&lt;td&gt;Source PDB must remain open read-only while clones exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exascale (23ai+)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Redirect-on-write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;"created quickly, consume little storage space upon initial creation, and can be created in practically unlimited numbers"&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the precision on "redirect-on-write" — that's Oracle's official term &lt;em&gt;only&lt;/em&gt; for Exascale snapshots in 23ai+. Older substrates use copy-on-write semantics. Per the &lt;a href="https://docs.oracle.com/en/learn/exadb-xs-pdb-snapshot/index.html" rel="noopener noreferrer"&gt;Exadata Database Service on Exascale Infrastructure documentation&lt;/a&gt;: &lt;em&gt;"These PDB snapshots leverage Exascale redirect-on-write technology so that they are created quickly, consume little storage space upon initial creation, and can be created in practically unlimited numbers."&lt;/em&gt; The distinction matters if you're going to argue with someone about it.&lt;/p&gt;

&lt;p&gt;Sibling features in the Multitenant family:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDB Snapshot Carousel&lt;/strong&gt; (introduced in &lt;strong&gt;18c&lt;/strong&gt;, not 19c — common citation error). Per &lt;a href="https://oracle-base.com/articles/18c/multitenant-pdb-snapshot-carousel-18c" rel="noopener noreferrer"&gt;oracle-base.com&lt;/a&gt;: &lt;em&gt;"Oracle 18c introduced the concept of a snapshot carousel, which is a series of point-in-time copies, or snapshots, of a PDB."&lt;/em&gt; Default 8 snapshots, hard cap at 8 via &lt;code&gt;MAX_PDB_SNAPSHOTS&lt;/code&gt;. Oldest is overwritten when full. Useful for short-horizon point-in-time recovery without the overhead of full backups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refreshable Clones.&lt;/strong&gt; Physically full copies with incremental redo apply. Different beast from snapshot copies (full storage cost, but ongoing sync from source). Convertible one-way to a regular PDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDB density.&lt;/strong&gt; Up to &lt;strong&gt;4098 PDBs per CDB&lt;/strong&gt; on Enterprise Edition with Multitenant licensing — the &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/MAX_PDBS.html" rel="noopener noreferrer"&gt;&lt;code&gt;MAX_PDBS&lt;/code&gt; reference&lt;/a&gt; lists possible values of &lt;code&gt;5&lt;/code&gt;, &lt;code&gt;254&lt;/code&gt;, or &lt;code&gt;4098&lt;/code&gt; by edition (Standard/Express, Standard Edition 2, Enterprise Edition respectively).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now compare ceilings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Branch limit&lt;/th&gt;
&lt;th&gt;Branch depth&lt;/th&gt;
&lt;th&gt;Cross-region&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Aurora&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15 copy-on-write clones per source; 16th becomes a full copy&lt;/td&gt;
&lt;td&gt;No explicit depth ceiling, but each level re-consumes the 15 budget&lt;/td&gt;
&lt;td&gt;&lt;em&gt;"You can't create a clone in a different AWS Region from the source Aurora DB cluster"&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Lakebase&lt;/strong&gt; (Databricks doc)&lt;/td&gt;
&lt;td&gt;500 per project; &lt;strong&gt;only 10 unarchived (active) at once&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Hundreds nested (per their telemetry)&lt;/td&gt;
&lt;td&gt;Per region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Oracle Multitenant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Up to 4098 PDBs per CDB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No documented depth limit&lt;/td&gt;
&lt;td&gt;RAC + Data Guard, cross-region via Active Data Guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lakebase's 500-per-project ceiling is generous compared to Aurora's 15. Oracle's 4098 is generous compared to Lakebase's 500 by an order of magnitude. And Lakebase has another hard cap that doesn't appear in the cloning side of the comparison: it allows only &lt;strong&gt;10 unarchived (active) branches at once&lt;/strong&gt;. Oracle has no equivalent active-cap; you tune branch density via Resource Manager based on your workload, which is the next section.&lt;/p&gt;

&lt;p&gt;This primitive shipped on July 1, 2013, in Oracle Database 12c. Twelve years before Lakebase. In the database engine, not in a wrapper. With a single SQL statement, documented in the official SQL Language Reference. There is no Postgres extension here. There is no separate page server, no Paxos quorum, no $1B acquisition. It's just &lt;code&gt;CREATE PLUGGABLE DATABASE … SNAPSHOT COPY&lt;/code&gt;, and it has been since the series finale of Breaking Bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compute Story Most People Get Wrong
&lt;/h2&gt;

&lt;p&gt;A note on this section: the structural argument here came from Patrick during a Slack thread when he challenged me on the scale-to-zero comparison. I had it wrong initially. Here's the correct read, in his voice.&lt;/p&gt;

&lt;p&gt;The naive comparison says Lakebase wins on scale-to-zero because branches scale individually to zero compute when idle. Oracle, the story goes, is "always on" — fixed ECPUs allocated to the ADB instance, multiple PDBs sharing the pool, no per-branch zero-cost dormancy.&lt;/p&gt;

&lt;p&gt;That comparison gets the shape right and the conclusion wrong.&lt;/p&gt;

&lt;p&gt;Yes, in Autonomous Database Serverless, ECPUs are allocated at the instance level, not per PDB. Yes, Snapshot Copy PDB branches inside an ADB share that pool. The naive read says: "uh-oh, no isolation, abandoned branches will eat compute." The correct read is: &lt;strong&gt;abandoned branches in a shared pool consume nothing by construction&lt;/strong&gt; — because they aren't reserving anything.&lt;/p&gt;

&lt;p&gt;Walk through the mechanics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Closed PDBs consume zero CPU and zero shadow processes.&lt;/strong&gt; &lt;code&gt;ALTER PLUGGABLE DATABASE foo CLOSE IMMEDIATE;&lt;/code&gt; and the branch is dormant. The 26c SQL Reference describes the semantic: &lt;em&gt;"the PDB equivalent of the SQL*Plus &lt;code&gt;SHUTDOWN&lt;/code&gt; command with the immediate mode."&lt;/em&gt; Metadata stays in the dictionary; nothing else stays resident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idle open PDBs consume near-zero.&lt;/strong&gt; Just metadata pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active PDBs draw from the shared pool.&lt;/strong&gt; That pool auto-scales: per the Oracle docs, &lt;em&gt;"with compute auto scaling enabled the database can use up to three times more CPU and IO resources than specified by the number of ECPUs."&lt;/em&gt; You pay for the burst when it happens, not when it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Manager governs the priority.&lt;/strong&gt; CPU shares, &lt;code&gt;MAX_IOPS&lt;/code&gt;, &lt;code&gt;MAX_MBPS&lt;/code&gt;, sessions, parallel servers, per-PDB &lt;code&gt;SGA_TARGET&lt;/code&gt; and &lt;code&gt;PGA_AGGREGATE_LIMIT&lt;/code&gt;. You decide which branches get more pool when contended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;V$PDBS&lt;/code&gt; and &lt;code&gt;V$RESOURCE_LIMIT&lt;/code&gt; expose per-branch consumption&lt;/strong&gt; so a supervisor process can watch and auto-suspend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So what's the real difference? Lakebase per-DB scale-to-zero with cold-start latency on resume. Oracle shared elastic pool with no cold start.&lt;/p&gt;

&lt;p&gt;For an agentic workflow, where the supervisor might wake an "abandoned" branch tomorrow to revisit a hypothesis it shelved today, &lt;strong&gt;the no-cold-start property matters.&lt;/strong&gt; The branch has been consuming nothing; the moment it gets a connection, it's responsive within milliseconds because the compute pool is already warm. Lakebase, by design, has to spin compute back up.&lt;/p&gt;

&lt;p&gt;Which means the elasticity scoreboard most people read off the spec sheet — &lt;em&gt;"Lakebase: scale-to-zero ✅ / Oracle: shared pool ❌"&lt;/em&gt; — is solving the same problem two different ways and pretending one wins. Different shape. Same economics for abandoned experiments. Faster wakeup on Oracle when the agent comes back.&lt;/p&gt;

&lt;p&gt;Sharing compute between PDBs isn't a bug. It means abandoned branches aren't wasting compute, period.&lt;/p&gt;

&lt;p&gt;Or as I put it in Slack when this came up: &lt;em&gt;"What we want is exactly what we already have. The compute is scaled. Abandoned branches contribute nothing."&lt;/em&gt; That's the architecture.&lt;/p&gt;

&lt;p&gt;— Patrick&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Limits
&lt;/h2&gt;

&lt;p&gt;Side-by-side, with citations on every claim:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Lakebase&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Oracle Multitenant + ADB&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total branches&lt;/td&gt;
&lt;td&gt;500 / project (&lt;a href="https://www.databricks.com/blog/database-branching-postgres-git-style-workflows-databricks-lakebase" rel="noopener noreferrer"&gt;Databricks doc&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Up to 4098 / CDB (&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/MAX_PDBS.html" rel="noopener noreferrer"&gt;MAX_PDBS&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active branches&lt;/td&gt;
&lt;td&gt;10 (hard cap)&lt;/td&gt;
&lt;td&gt;No hard cap; tuned via Resource Manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch creation speed&lt;/td&gt;
&lt;td&gt;Instant (metadata + COW)&lt;/td&gt;
&lt;td&gt;Near-instant on snapshot-capable storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start on resume&lt;/td&gt;
&lt;td&gt;Sub-second to multi-second&lt;/td&gt;
&lt;td&gt;None — shared pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACID&lt;/td&gt;
&lt;td&gt;Postgres MVCC&lt;/td&gt;
&lt;td&gt;Full ACID, RAC, Active Data Guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover behavior&lt;/td&gt;
&lt;td&gt;Postgres-standard (kills in-flight)&lt;/td&gt;
&lt;td&gt;Transparent Application Continuity — in-flight transaction replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector search&lt;/td&gt;
&lt;td&gt;Postgres extension&lt;/td&gt;
&lt;td&gt;In-engine, optimized by 40-year-old CBO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;jsonb (sequential traversal)&lt;/td&gt;
&lt;td&gt;OSON binary, hash-indexed O(1) field access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph&lt;/td&gt;
&lt;td&gt;Postgres extension&lt;/td&gt;
&lt;td&gt;SQL/PGQ, in-engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-modal queries (vector + JSON + graph + relational)&lt;/td&gt;
&lt;td&gt;Limited by extension boundaries&lt;/td&gt;
&lt;td&gt;Single transaction, single query plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open data format&lt;/td&gt;
&lt;td&gt;"Postgres page on S3" (Postgres-only readable)&lt;/td&gt;
&lt;td&gt;OSON + Iceberg + Parquet + Mongo wire + native SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mongo wire compatibility&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes (Oracle MongoDB API)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Lakebase wins on developer-experience polish today.&lt;/strong&gt; The branching UX is wired into the product, the CLI is published, the dashboard renders branch trees. Credit where due — that's a real product investment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oracle wins on every limit that matters once you stop counting GitHub stars.&lt;/strong&gt; Density (4098 vs 500). Active concurrency (no cap vs 10). ACID. Failover that doesn't kill your transactions. Vector + JSON + graph + spatial + relational in one query plan optimized by 40 years of CBO development. Mongo wire compatibility, for the developers who already wrote against MongoDB and don't want to rewrite their app to evaluate a database.&lt;/p&gt;

&lt;p&gt;The DX gap is real. It's also the easiest gap to close, which is the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DX Gap, And Why It's Trivial to Close
&lt;/h2&gt;

&lt;p&gt;Patrick said it best in the original Slack thread: &lt;em&gt;"We probably should develop a lightweight external API too. That should be extremely simple — it's all external to the database."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;He was right and he's already shipped it.&lt;/p&gt;

&lt;p&gt;The DX gap is real. There is no &lt;code&gt;pdb branch my-experiment&lt;/code&gt; command in stock Oracle. Lakebase has a polished branching UX with a published CLI, a dashboard, and &lt;code&gt;git&lt;/code&gt;-shaped semantics. We're not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;But this is a wrapper-shaped problem, not a kernel-shaped problem. Patrick built the wrapper:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/pmeredit/pdb-branch" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;pmeredit/pdb-branch&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; — &lt;em&gt;"a small multi-language library over a shared PL/SQL package for making Oracle PDB snapshot copies feel like cheap database branches for agentic workflow experiments."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Python, Node.js, Rust, and Java bindings, plus a Rust-built &lt;code&gt;pdb&lt;/code&gt; CLI. Releases alongside this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The architecture is small enough to fit on a napkin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PDB_BRANCH&lt;/code&gt; PL/SQL package&lt;/strong&gt; — installed and upgraded automatically by the language binding at startup. Wraps &lt;code&gt;CREATE PLUGGABLE DATABASE … SNAPSHOT COPY&lt;/code&gt; with idempotent lifecycle DDL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three control tables in &lt;code&gt;CDB$ROOT&lt;/code&gt;:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PDB_BRANCH_BRANCHES&lt;/code&gt; — branch registry (name, parent, state, expiration, score)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PDB_BRANCH_EVENTS&lt;/code&gt; — audit log of branch lifecycle events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PDB_BRANCH_PROFILES&lt;/code&gt; — branch-to-Resource-Manager-profile mapping&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;&lt;code&gt;BranchClient&lt;/code&gt; wrappers in four languages&lt;/strong&gt; — Python over &lt;code&gt;python-oracledb&lt;/code&gt;, Node.js over &lt;code&gt;oracledb&lt;/code&gt;, Rust over the ODPI-C-based &lt;code&gt;oracle&lt;/code&gt; crate (with a pure-Rust &lt;code&gt;oracle-rs&lt;/code&gt; path for non-SYSDBA work), and Java. One PL/SQL contract, four idiomatic surfaces.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;A &lt;code&gt;pdb&lt;/code&gt; Rust CLI&lt;/strong&gt; — &lt;code&gt;bin/pdb&lt;/code&gt; wraps the Rust binding so callers don't need to know Cargo's &lt;code&gt;target/&lt;/code&gt; layout. &lt;code&gt;git branch&lt;/code&gt;-shaped commands, &lt;code&gt;.pdbprofile&lt;/code&gt; TOML config, and per-flag environment-variable overrides.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Optional Resource Manager profiles:&lt;/strong&gt; &lt;code&gt;PDB_BRANCH_ACTIVE&lt;/code&gt;, &lt;code&gt;PDB_BRANCH_IDLE&lt;/code&gt;, &lt;code&gt;PDB_BRANCH_BACKGROUND&lt;/code&gt;.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Two ways to drive it. The library surface (Python shown; Node/Rust/Java are equivalents):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pdb_branch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BranchClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BranchClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# auto-installs/upgrades PL/SQL package
&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_branch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_RAG_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;from_pdb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GOLDEN_MASTER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;try smaller chunk size and rerank before answer synthesis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_RAG_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval: qa_regression_v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_RAG_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;winner for current retrieval policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_idle_after_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drop_expired&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, at the shell, the same workflow via the &lt;code&gt;pdb&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/pdb init &lt;span class="nt"&gt;--dsn&lt;/span&gt; localhost:1521/FREE &lt;span class="nt"&gt;--user&lt;/span&gt; sys &lt;span class="nt"&gt;--password&lt;/span&gt; ... &lt;span class="nt"&gt;--from&lt;/span&gt; FREEPDB1
bin/pdb branch AGENT_RAG_042 &lt;span class="nt"&gt;--notes&lt;/span&gt; &lt;span class="s2"&gt;"try smaller chunk size and rerank"&lt;/span&gt;
bin/pdb score   AGENT_RAG_042 0.91 &lt;span class="nt"&gt;--notes&lt;/span&gt; &lt;span class="s2"&gt;"eval: qa_regression_v3"&lt;/span&gt;
bin/pdb promote AGENT_RAG_042
bin/pdb branch &lt;span class="nt"&gt;-d&lt;/span&gt; AGENT_RAG_042
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bin/pdb init&lt;/code&gt; writes a &lt;code&gt;.pdbprofile&lt;/code&gt; so the daily commands stay short. The CLI also accepts environment-variable overrides and flag overrides — flags beat env vars beat &lt;code&gt;.pdbprofile&lt;/code&gt; beat local defaults.&lt;/p&gt;

&lt;p&gt;That's the entire developer experience. Branch, score, promote, reap. The argument that Oracle "doesn't have &lt;code&gt;git branch&lt;/code&gt; for databases" was true a week ago. Today there's a CLI in the repo, an integration test that runs it against an Oracle Free container in CI, and a Rust binary you can drop in your &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One architectural point worth elevating: the two-connection security model.&lt;/strong&gt; The agent never gets &lt;code&gt;SYSDBA&lt;/code&gt;. There are two distinct connections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Control-plane connection&lt;/strong&gt; — trusted orchestration code → &lt;code&gt;CDB$ROOT&lt;/code&gt; as &lt;code&gt;SYSDBA&lt;/code&gt; → uses &lt;code&gt;BranchClient&lt;/code&gt; to create, open, close, and drop PDB branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workload connection&lt;/strong&gt; — the agent → branch PDB → normal application user → ordinary SQL against branch-local data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent receives only a DSN to its assigned branch and standard application credentials. It cannot create branches, drop branches, or escape its sandbox. Lakebase has nothing analogous in its branching API today; the agent-vs-supervisor security boundary is enforced at the cloud-IAM layer rather than in the database itself, and that's a category weaker than separation of concerns enforced inside the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot-copy fallback is engineered, not aspirational.&lt;/strong&gt; When the library requests &lt;code&gt;SNAPSHOT COPY&lt;/code&gt; and the underlying storage rejects it — Oracle Free's container filesystem returns &lt;code&gt;ORA-17525&lt;/code&gt; / &lt;code&gt;ORA-65169&lt;/code&gt;, for instance — the library transparently retries as a full clone, records a &lt;code&gt;SNAPSHOT_COPY_FALLBACK&lt;/code&gt; row in &lt;code&gt;PDB_BRANCH_EVENTS&lt;/code&gt;, and (in the Python binding) emits a &lt;code&gt;SnapshotCopyFallbackWarning&lt;/code&gt;. Correctness is preserved on substrates that can't sparse-clone; the events table makes it visible when that happened so capacity planning isn't a guessing game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free deployment path:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oracle Database 23ai/26ai Free Docker image&lt;/strong&gt; — &lt;code&gt;container-registry.oracle.com/database/free&lt;/code&gt;. CDB service &lt;code&gt;FREE&lt;/code&gt;, default PDB &lt;code&gt;FREEPDB1&lt;/code&gt;. Multiple branch PDBs supported. The Free image's container filesystem doesn't support storage snapshots, so &lt;code&gt;snapshot_copy=True&lt;/code&gt; is silently treated as a full clone via the fallback path above — which means 10–30 branches realistic on a laptop, not hundreds. &lt;strong&gt;$0 cost forever&lt;/strong&gt;, and the Oracle Free integration tests in the repo run the Python, Node.js, Rust, Java, and CLI surfaces against this image in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-managed CDB on 19c+ with snapshot-capable storage&lt;/strong&gt; — production target. ACFS, dNFS, Exadata sparse, or Exascale. Branch DDL uses Oracle Managed Files via &lt;code&gt;CREATE_FILE_DEST&lt;/code&gt;, preferring &lt;code&gt;DB_CREATE_FILE_DEST&lt;/code&gt; when set and otherwise deriving a destination from the parent PDB's datafile directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADB Serverless / Always Free is explicitly NOT a v1 target.&lt;/strong&gt; ADB application connections land in an existing PDB, not in &lt;code&gt;CDB$ROOT&lt;/code&gt;, so they cannot run PDB branch DDL. A real architectural constraint of ADB's tenancy model, not a &lt;code&gt;pdb-branch&lt;/code&gt; limitation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README is honest about v1 boundaries: the idempotent installer doesn't migrate destructive schema changes yet; PL/SQL identifiers are restricted to simple unquoted Oracle names; promotion is metadata-only, with scaling and export workflows left to deployment-specific adapters. That's an honest v1 scope.&lt;/p&gt;

&lt;p&gt;The article is the "why." The repo is the "how." They land together, today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agentic Workflow on Oracle
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr7ppv8v23xv2yy5zmx5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr7ppv8v23xv2yy5zmx5w.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lifecycle Patrick described in our Slack thread, mapped to the actual &lt;code&gt;pdb-branch&lt;/code&gt; API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — heavy experimentation.&lt;/strong&gt; The supervisor holds the SYSDBA control-plane connection and spins up branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hypothesis&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hypotheses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_branch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hypothesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;from_pdb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GOLDEN_MASTER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hypothesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hypothesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDB_BRANCH_ACTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent receives a DSN to its assigned branch plus an app-user credential. Agents do not see &lt;code&gt;CDB$ROOT&lt;/code&gt;. They run their experiments — vector queries, JSON queries, SQL, whatever the eval needs — against ordinary Oracle PDBs. Once the branch PDB is open there is no special "branch query mode": the branch is just an isolated Oracle PDB service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — evaluate.&lt;/strong&gt; Supervisor logs scores back to &lt;code&gt;PDB_BRANCH_BRANCHES&lt;/code&gt; as agents finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_RAG_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval: qa_regression_v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The supervisor process can watch &lt;code&gt;V$PDBS&lt;/code&gt; (open mode, last open time, total size) and &lt;code&gt;V$RESOURCE_LIMIT&lt;/code&gt; (per-PDB CPU and I/O draw) for liveness and resource consumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — promote and reap.&lt;/strong&gt; Winners stay active. Losers get downgraded or closed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_RAG_042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;winner for current retrieval policy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;close_idle_after_minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drop_expired&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cleanup&lt;/code&gt; is the auto-suspend / auto-drop primitive. In production you don't run this from the supervisor; you schedule &lt;code&gt;PDB_BRANCH.CLEANUP&lt;/code&gt; from &lt;code&gt;DBMS_SCHEDULER&lt;/code&gt; so the orchestration code doesn't need to babysit branch lifecycle.&lt;/p&gt;

&lt;p&gt;Behind those four method calls, the SQL is exactly what you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;AGENT_RAG_042&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;GOLDEN_MASTER&lt;/span&gt; &lt;span class="n"&gt;SNAPSHOT&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;AGENT_RAG_042&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;AGENT_RAG_042&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;DB_PERFORMANCE_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'PDB_BRANCH_ACTIVE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;PDB_BRANCH_BRANCHES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PARENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;STATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AGENT_RAG_042'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GOLDEN_MASTER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'try smaller chunk size...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;PDB_BRANCH_EVENTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BRANCH_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EVENT_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DETAILS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EVENT_TIME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AGENT_RAG_042'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'CREATED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"from":"GOLDEN_MASTER"}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five statements, one transaction. The branch is live. An agent connects to &lt;code&gt;AGENT_RAG_042&lt;/code&gt; as &lt;code&gt;app_user&lt;/code&gt; and runs its experiment.&lt;/p&gt;

&lt;p&gt;This is what Databricks calls evolutionary algorithms in the database. It's the right framing. The substrate has been Oracle for a decade; what was missing was the wrapper that makes it feel like git. Each language binding is roughly one module long, the Rust &lt;code&gt;pdb&lt;/code&gt; CLI is one binary, and they all sit on top of one shared PL/SQL package. The whole DX gap was about that much code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Reality
&lt;/h2&gt;

&lt;p&gt;Both platforms have real costs and real free entry points. Skipping the marketing-deck pricing slide and going straight to what an engineer would actually pay:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload pattern&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Lakebase&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Oracle ADB Serverless 2 ECPU&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 mostly-idle branches, occasional bursts&lt;/td&gt;
&lt;td&gt;$80–$150/mo&lt;/td&gt;
&lt;td&gt;$190–$290/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100+ branches, high density&lt;/td&gt;
&lt;td&gt;Hits the 10-active wall&lt;/td&gt;
&lt;td&gt;Scales naturally to thousands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustained 8+ hr/day activity&lt;/td&gt;
&lt;td&gt;Capacity-unit cost climbs&lt;/td&gt;
&lt;td&gt;Cheaper at sustained load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage at scale&lt;/td&gt;
&lt;td&gt;$0.345 / GB-month&lt;/td&gt;
&lt;td&gt;~$0.024 / GB-month (≈15× cheaper)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free for prototyping&lt;/td&gt;
&lt;td&gt;Always Free tier (limited)&lt;/td&gt;
&lt;td&gt;Free Docker image: $0 forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are public list prices as of mid-2026, picked from each vendor's published rates. Run the numbers for your workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest read:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lakebase wins on bursty, mostly-idle floors with light data.&lt;/strong&gt; That's the optimization point of per-DB scale-to-zero, and they do it well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle wins on density, sustained activity, and storage at scale.&lt;/strong&gt; When agents are actually doing work, the shared-pool model delivers more compute per dollar. When experiment data grows, the storage cost differential alone (~15×) can dominate the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle Free Docker is genuinely free.&lt;/strong&gt; No cloud signup, no credit card, no quotas. Patrick's &lt;code&gt;pdb-branch&lt;/code&gt; README documents this as the recommended local prototyping path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the compute story restated as economics. Per-DB scale-to-zero looks cheap when nothing is running. Shared elastic pool is cheaper when anything is running. Pick the model that matches your workload, not the marketing scoreboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's &lt;em&gt;Actually&lt;/em&gt; New About Lakebase
&lt;/h2&gt;

&lt;p&gt;Worth giving Databricks an honest hearing. The "third-generation" framing collapses the moment you check the dates. What about their other claim — that in Lakebase &lt;em&gt;"both the storage infrastructure and the data formats are completely open"&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;That one survives partway and dies in the details.&lt;/p&gt;

&lt;p&gt;The operational store in Lakebase is &lt;strong&gt;Postgres page format on cloud object storage.&lt;/strong&gt; That's what they mean by "open storage infrastructure." But Postgres' on-disk page layout is a physical storage format, not a portable interchange format. The only thing that can &lt;em&gt;read&lt;/em&gt; a Postgres page file is the Postgres engine. Calling that "open" because the Postgres source code is open is a category error. By that logic, MongoDB's BSON is "open" because the spec is published.&lt;/p&gt;

&lt;p&gt;The other openness claim — that the same data is queryable as Iceberg by external analytical engines — is true. But the Iceberg view isn't the operational store. It's a &lt;strong&gt;separate projection layer&lt;/strong&gt; (the "Mooncake" bridge — Databricks' OLTP-to-lakehouse export pipeline). Iceberg files are derived from the operational Postgres pages, not the same bytes.&lt;/p&gt;

&lt;p&gt;Which means Lakebase's actual architecture is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;canonical store&lt;/strong&gt; in Postgres-only page format. Closed to anything that isn't Postgres.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;projected shape&lt;/strong&gt; in Iceberg, exported to make the data analytically accessible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's exactly &lt;strong&gt;canonical form + projected shape.&lt;/strong&gt; It's the architecture pattern I've been calling Unified Model Theory for the last two years. Databricks reinvented UMT, called the closed canonical store "open," and called the projection layer "openness."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq0c9bw7zp6k2jz9n8xs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq0c9bw7zp6k2jz9n8xs.png" alt=" " width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oracle's answer to "open data" is the converged engine itself: same canonical store, multiple shapes natively in the engine — SQL, JSON Duality Views, Property Graph, Vector, Spatial, Full-Text Search, Mongo wire protocol, OSON serialization out, Iceberg/Parquet for analytics. No bridge layer required. The cost-based optimizer sees all the modalities in a single query plan.&lt;/p&gt;

&lt;p&gt;The architecture-correct way to expose canonical data through multiple shapes is to do it in the engine. That is what Oracle has been shipping for 40 years and what UMT formalizes. Databricks' Lakebase + Mooncake architecture is one valid implementation pattern of the same idea, with two extra hops and a new vocabulary.&lt;/p&gt;

&lt;p&gt;What's actually new in Lakebase isn't the architecture. It's the &lt;strong&gt;packaging&lt;/strong&gt; — a polished branching UX wired into a data lake brand and a billion dollars of marketing oxygen. That's a real product investment and a credible push into a market segment Oracle has under-marketed. Credit where due.&lt;/p&gt;

&lt;p&gt;It's just not "third-generation database architecture." It's first-generation Postgres branching with a second-generation marketing department.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Take
&lt;/h2&gt;

&lt;p&gt;Three things to land:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Agents do need branching.&lt;/strong&gt; Databricks' diagnosis is correct, and the agentic future they describe is real. Database branching is the missing primitive for evolutionary development. Cost floors do break the economics. Storage and compute do need to scale independently. Credit where due.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Lakebase is competent execution of an idea Oracle Multitenant solved in 2013.&lt;/strong&gt; Neon is good engineering. Lakebase is Neon plus a brand and a UX layer. That's fine — but it isn't "third generation." It's a four-year-old Postgres-branching architecture, recently acquired and rebranded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The architecture-correct version exists today.&lt;/strong&gt; Full ACID. Up to 4098 branches per CDB. Vector, graph, JSON, spatial, full-text — single engine, single transaction, single query plan optimized by 40 years of cost-based optimizer development. Transparent Application Continuity replays in-flight transactions across failover. The two-connection security model keeps agents out of &lt;code&gt;CDB$ROOT&lt;/code&gt; by construction.&lt;/p&gt;

&lt;p&gt;The only real gap was developer experience. Patrick's &lt;a href="https://github.com/pmeredit/pdb-branch" rel="noopener noreferrer"&gt;&lt;code&gt;pdb-branch&lt;/code&gt;&lt;/a&gt; closes it. &lt;strong&gt;Today.&lt;/strong&gt; A Python client, a PL/SQL package, three control tables, and a sane API. Branch, score, promote, reap.&lt;/p&gt;

&lt;p&gt;Stop reinventing 2013. Build the wrapper. Ship.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Third-generation database architecture? We're on our fifth.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;— Rick &amp;amp; Patrick&lt;/p&gt;




&lt;h2&gt;
  
  
  Citations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Databricks (primary subject):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How agentic software development will change databases" — &lt;a href="https://www.databricks.com/blog/how-agentic-software-development-will-change-databases" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/how-agentic-software-development-will-change-databases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"A New Era of Databases: Lakebase" (June 12, 2025) — &lt;a href="https://www.databricks.com/blog/what-is-a-lakebase" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/what-is-a-lakebase&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"How lakebase architecture delivers 5x faster Postgres writes" — &lt;a href="https://www.databricks.com/blog/how-lakebase-architecture-delivers-5x-faster-postgres-writes" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/how-lakebase-architecture-delivers-5x-faster-postgres-writes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Database Branching in Postgres: Git-Style Workflows" — &lt;a href="https://www.databricks.com/blog/database-branching-postgres-git-style-workflows-databricks-lakebase" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/database-branching-postgres-git-style-workflows-databricks-lakebase&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;"Databricks Agrees to Acquire Neon" press release (May 14, 2025) — &lt;a href="https://www.databricks.com/company/newsroom/press-releases/databricks-agrees-acquire-neon-help-developers-deliver-ai-systems" rel="noopener noreferrer"&gt;https://www.databricks.com/company/newsroom/press-releases/databricks-agrees-acquire-neon-help-developers-deliver-ai-systems&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Oracle Database documentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;12c Multitenant Concepts — &lt;a href="https://docs.oracle.com/database/121/CNCPT/cdbovrvw.htm" rel="noopener noreferrer"&gt;https://docs.oracle.com/database/121/CNCPT/cdbovrvw.htm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;19c CREATE PLUGGABLE DATABASE — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-PLUGGABLE-DATABASE.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-PLUGGABLE-DATABASE.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;19c Cloning a PDB — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/cloning-a-pdb.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/cloning-a-pdb.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;19c Administering a PDB Snapshot Carousel — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/administering-pdb-snapshots.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/administering-pdb-snapshots.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;19c MAX_PDBS reference — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/MAX_PDBS.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/refrn/MAX_PDBS.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;21c V$PDBS reference — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/V-PDBS.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/V-PDBS.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;26c ALTER PLUGGABLE DATABASE — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/ALTER-PLUGGABLE-DATABASE.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/ALTER-PLUGGABLE-DATABASE.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Resource Manager for PDBs (19c) — &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/using-oracle-resource-manager-for-pdbs-with-sql-plus.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/database/oracle/oracle-database/19/multi/using-oracle-resource-manager-for-pdbs-with-sql-plus.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ADB Compute Models (ECPU/OCPU) — &lt;a href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/autonomous-compute-models.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/autonomous-compute-models.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ADB Auto-Scale 3× — &lt;a href="https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/autonomous-auto-scale.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/autonomous-auto-scale.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PDB Snapshots on Exadata Exascale (23ai+) — &lt;a href="https://docs.oracle.com/en/learn/exadb-xs-pdb-snapshot/index.html" rel="noopener noreferrer"&gt;https://docs.oracle.com/en/learn/exadb-xs-pdb-snapshot/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Historical context:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dremel 2020 retrospective (VLDB) — &lt;a href="https://www.vldb.org/pvldb/vol13/p3461-melnik.pdf" rel="noopener noreferrer"&gt;https://www.vldb.org/pvldb/vol13/p3461-melnik.pdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Aurora 10-year retrospective — &lt;a href="https://aws.amazon.com/blogs/aws/celebrating-10-years-of-amazon-aurora-innovation/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/aws/celebrating-10-years-of-amazon-aurora-innovation/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Aurora cloning hard limits — &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Snowflake architecture — &lt;a href="https://docs.snowflake.com/en/user-guide/intro-key-concepts" rel="noopener noreferrer"&gt;https://docs.snowflake.com/en/user-guide/intro-key-concepts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Oracle 18c PDB Snapshot Carousel introduction — &lt;a href="https://oracle-base.com/articles/18c/multitenant-pdb-snapshot-carousel-18c" rel="noopener noreferrer"&gt;https://oracle-base.com/articles/18c/multitenant-pdb-snapshot-carousel-18c&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Neon / Postgres branching:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neon architecture overview — &lt;a href="https://neon.com/docs/introduction/architecture-overview" rel="noopener noreferrer"&gt;https://neon.com/docs/introduction/architecture-overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Neon branching docs — &lt;a href="https://neon.com/docs/introduction/branching" rel="noopener noreferrer"&gt;https://neon.com/docs/introduction/branching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TechTarget on Databricks/Neon acquisition — &lt;a href="https://www.techtarget.com/searchdatamanagement/news/366623864/Databricks-adds-Postgres-database-with-1B-Neon-acquisition" rel="noopener noreferrer"&gt;https://www.techtarget.com/searchdatamanagement/news/366623864/Databricks-adds-Postgres-database-with-1B-Neon-acquisition&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Companion repository:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pmeredit/pdb-branch — &lt;a href="https://github.com/pmeredit/pdb-branch" rel="noopener noreferrer"&gt;https://github.com/pmeredit/pdb-branch&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>lakebase</category>
      <category>databricks</category>
      <category>oracle</category>
    </item>
    <item>
      <title>Oracle SQL/JSON: The Developer's Guide to Querying JSON Like a Pro</title>
      <dc:creator>Rick Houlihan</dc:creator>
      <pubDate>Wed, 15 Apr 2026 14:12:57 +0000</pubDate>
      <link>https://forem.com/rick_houlihan_cf110dba340/oracle-sqljson-the-developers-guide-to-querying-json-like-a-pro-3hmf</link>
      <guid>https://forem.com/rick_houlihan_cf110dba340/oracle-sqljson-the-developers-guide-to-querying-json-like-a-pro-3hmf</guid>
      <description>&lt;p&gt;SQL and JSON aren't enemies. They never were.&lt;/p&gt;

&lt;p&gt;For a decade, the industry sold you a false choice: pick the rigidity of relational tables or the flexibility of JSON documents. Build your app on SQL or build it on a document store. Structure or freedom. Choose.&lt;/p&gt;

&lt;p&gt;It was never a real tradeoff. It was a failure of implementation masquerading as a law of nature.&lt;/p&gt;

&lt;p&gt;Oracle's SQL/JSON support — built into the database engine since 12c and dramatically expanded through 19c, 21c, and 26ai — proves the point. You get the full expressive power of SQL (joins, aggregations, window functions, a 40-year-old cost-based optimizer) and the full flexibility of JSON (nested documents, arrays, schema-optional structures) in the same query, the same transaction, the same execution plan.&lt;/p&gt;

&lt;p&gt;This article is a practical developer's guide. We'll start simple and build to sophisticated. By the end, you'll understand CTEs, every major SQL/JSON function, and how they compose into queries that would require five different databases in a polyglot architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run It Yourself
&lt;/h2&gt;

&lt;p&gt;Every SQL example below is executed end-to-end against a live Oracle 26ai database by the article's companion validator: &lt;strong&gt;&lt;a href="https://github.com/rhoulihan/json-sql-guide" rel="noopener noreferrer"&gt;rhoulihan/json-sql-guide&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The validator extracts every fenced SQL block, runs it against Oracle 26ai Free in a Docker container, and produces an annotated copy of this article with a green ✓ or red ✗ badge after every snippet. If a Pull Request to this article ever introduces a broken example, CI catches it before merge.&lt;/p&gt;

&lt;p&gt;If you want to follow along in your own database — or contribute a fix — clone the repo, boot Oracle 26ai Free, and run &lt;code&gt;validator run&lt;/code&gt; against this markdown file. The README walks through it. Every sample table, every seed row, every CREATE INDEX in this article comes from there.&lt;/p&gt;

&lt;p&gt;Let's build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Sample Data
&lt;/h2&gt;

&lt;p&gt;Everything in this article runs against a single table. Simple enough to follow, rich enough to be real. The companion repository (&lt;a href="https://github.com/rhoulihan/json-sql-guide" rel="noopener noreferrer"&gt;rhoulihan/json-sql-guide&lt;/a&gt;) provides this schema and sample data — &lt;code&gt;make setup &amp;amp;&amp;amp; validator run&lt;/code&gt; will create it for you. The schema is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orders (
  id         NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_doc  JSON
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the column type: &lt;code&gt;JSON&lt;/code&gt;. Not &lt;code&gt;VARCHAR2&lt;/code&gt;. Not &lt;code&gt;CLOB&lt;/code&gt;. The native JSON data type — introduced in 21c — stores documents in Oracle's OSON binary format. Hash-indexed field navigation. O(1) access to any field at any depth. This matters more than most developers realize, and we'll come back to why.&lt;/p&gt;

&lt;p&gt;Documents in &lt;code&gt;order_doc&lt;/code&gt; follow this shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "orderId":   1001,
  "customer":  {"name": "Acme Corp", "tier": "platinum",
                "address": {"city": "Belmont", "state": "CA", "zip": "94002"}
               },
  "status":    "shipped",
  "priority":  "high",
  "orderDate": "2025-03-15",
  "shipping":  {"method": "express",
                "address": {"city": "Austin", "state": "TX", "zip": "78701"}
               },
  "items":     [{"product": "Widget Pro",  "quantity": 10, "unitPrice": 29.99, "category": "hardware"},
                {"product": "Gadget Plus", "quantity": 5,  "unitPrice": 49.99, "category": "electronics"},
                {"product": "Cable Kit",   "quantity": 50, "unitPrice": 4.99,  "category": "accessories"}],
  "tags":      ["wholesale", "priority", "Q1-promo"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validator loads ten such documents — different customers, statuses, item arrays, and tag combinations — so every filter and projection example below returns meaningful results. To follow along in your own database, clone the companion repo, boot Oracle 26ai Free, and run &lt;code&gt;validator run&lt;/code&gt; against this article.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Dot-Notation: The Easy On-Ramp
&lt;/h2&gt;

&lt;p&gt;If you've never queried JSON in Oracle, start here. Dot-notation gives you direct field access using the syntax you'd expect from any programming language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Table alias, JSON column, dot-separated field path. Oracle navigates the OSON binary structure, hashes the field names, jumps directly to the offsets. No parsing. No scanning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types are preserved.&lt;/strong&gt; When the column is declared as &lt;code&gt;JSON&lt;/code&gt; (the native OSON type), Oracle knows the underlying types of your values. A number stored as a JSON number comes back as a number. A string comes back as a string. You can use dot-notation results directly in comparisons, arithmetic, and predicates — Oracle handles the type mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Types flow naturally — no casting required&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- numeric comparison works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Item methods&lt;/strong&gt; let you &lt;em&gt;coerce&lt;/em&gt; types when you need explicit control — converting to a specific SQL type or applying a transformation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;             &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;item_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'high'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction matters: you don't &lt;em&gt;have&lt;/em&gt; to call &lt;code&gt;.string()&lt;/code&gt; to use a string value as a string. But item methods give you precision when you need it — &lt;code&gt;.date()&lt;/code&gt; parses a JSON string into a SQL &lt;code&gt;DATE&lt;/code&gt; you can use in date arithmetic, &lt;code&gt;.number()&lt;/code&gt; ensures numeric precision for financial calculations, and methods like &lt;code&gt;.upper()&lt;/code&gt;, &lt;code&gt;.size()&lt;/code&gt;, and &lt;code&gt;.sum()&lt;/code&gt; apply transformations inline without a separate function call.&lt;/p&gt;

&lt;p&gt;Available item methods include: &lt;code&gt;.string()&lt;/code&gt;, &lt;code&gt;.number()&lt;/code&gt;, &lt;code&gt;.date()&lt;/code&gt;, &lt;code&gt;.timestamp()&lt;/code&gt;, &lt;code&gt;.boolean()&lt;/code&gt;, &lt;code&gt;.double()&lt;/code&gt;, &lt;code&gt;.length()&lt;/code&gt;, &lt;code&gt;.upper()&lt;/code&gt;, &lt;code&gt;.lower()&lt;/code&gt;, &lt;code&gt;.size()&lt;/code&gt;, &lt;code&gt;.type()&lt;/code&gt;, &lt;code&gt;.abs()&lt;/code&gt;, &lt;code&gt;.ceiling()&lt;/code&gt;, &lt;code&gt;.floor()&lt;/code&gt;, &lt;code&gt;.count()&lt;/code&gt;, &lt;code&gt;.sum()&lt;/code&gt;, &lt;code&gt;.avg()&lt;/code&gt;, &lt;code&gt;.minNumber()&lt;/code&gt;, &lt;code&gt;.maxNumber()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's not a thin wrapper around JSON. That's a full type conversion system built into the path expression itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Array access&lt;/strong&gt; works how you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- First item&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;first_product&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- All items (returns a JSON array)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;all_products&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Last item — dot-notation doesn't recognize [last], so reach for&lt;/span&gt;
&lt;span class="c1"&gt;-- JSON_VALUE or JSON_QUERY when you need it&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[last].product'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_product&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dot-notation is perfect for quick queries, dashboards, and ad-hoc exploration. When you need more control — type safety, error handling, complex path expressions — you reach for the SQL/JSON functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. JSON_VALUE: Surgical Scalar Extraction
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;JSON_VALUE&lt;/code&gt; extracts a single scalar value from a JSON document and returns it as a SQL type. Think of it as dot-notation's more disciplined sibling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.shipping.address.zip'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;zip_code&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why use JSON_VALUE when dot-notation exists?
&lt;/h3&gt;

&lt;p&gt;Dot-notation gives you type preservation and coercion via item methods. &lt;code&gt;JSON_VALUE&lt;/code&gt; adds two things dot-notation can't do: &lt;strong&gt;error handling&lt;/strong&gt; and &lt;strong&gt;SQL-standard portability&lt;/strong&gt; (it's part of the ISO SQL/JSON spec, so your queries translate across databases that support the standard).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error handling&lt;/strong&gt; saves you at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- NULL ON ERROR (default): missing path returns NULL silently&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.nonexistent'&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nullable_value&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt;  &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ERROR ON ERROR: missing path raises ORA-40462 (shown as reference;&lt;/span&gt;
&lt;span class="c1"&gt;-- uncomment to see the error in action)&lt;/span&gt;
&lt;span class="c1"&gt;-- SELECT JSON_VALUE(o.order_doc, '$.nonexistent' ERROR ON ERROR)&lt;/span&gt;
&lt;span class="c1"&gt;-- FROM   orders o;&lt;/span&gt;

&lt;span class="c1"&gt;-- DEFAULT ... ON ERROR: missing path returns your fallback&lt;/span&gt;
&lt;span class="c1"&gt;-- (RETURNING comes before DEFAULT; order matters)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.discount'&lt;/span&gt;
                  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt;  &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same three options — &lt;code&gt;NULL&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;, &lt;code&gt;DEFAULT&lt;/code&gt; — work for &lt;code&gt;ON EMPTY&lt;/code&gt; (path exists but matches nothing). During development, use &lt;code&gt;ERROR ON ERROR&lt;/code&gt; to catch path mistakes early. In production, choose the behavior that matches your business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip (26ai+):&lt;/strong&gt; Instead of adding error clauses to every function, set the session-level default and clear it when you're done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;JSON_BEHAVIOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON_ERROR:ERROR'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ... debug session ...&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;JSON_BEHAVIOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ON_ERROR:NULL'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies to &lt;strong&gt;all SQL/JSON functions&lt;/strong&gt; in the session — &lt;code&gt;JSON_VALUE&lt;/code&gt;, &lt;code&gt;JSON_QUERY&lt;/code&gt;, &lt;code&gt;JSON_TABLE&lt;/code&gt;, and &lt;code&gt;JSON_EXISTS&lt;/code&gt; all inherit the error default. Any function that doesn't have an explicit &lt;code&gt;ON ERROR&lt;/code&gt; clause now raises errors instead of silently returning NULL. The second statement above resets back to the permissive default; in production the most common pattern is to leave the session at &lt;code&gt;ON_ERROR:NULL&lt;/code&gt; and add explicit &lt;code&gt;ERROR ON ERROR&lt;/code&gt; only on the specific paths where you want strict validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indexability&lt;/strong&gt; is where both &lt;code&gt;JSON_VALUE&lt;/code&gt; and dot-notation earn their keep in production. You can create functional B-tree indexes on JSON fields using either syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Functional index using JSON_VALUE&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_customer&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Equivalent index using dot-notation&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The type-matching gotcha:&lt;/strong&gt; The &lt;code&gt;RETURNING&lt;/code&gt; type in your index definition and your query must match exactly. If you create the index with &lt;code&gt;RETURNING NUMBER&lt;/code&gt; but your query omits the &lt;code&gt;RETURNING&lt;/code&gt; clause (which defaults to &lt;code&gt;VARCHAR2&lt;/code&gt;), the CBO won't recognize them as the same expression — and your index sits unused. This is the single most common SQL/JSON performance mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Index defined with RETURNING NUMBER&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- This query USES the index (types match)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- This query IGNORES the index — JSON_VALUE defaults to VARCHAR2, not NUMBER&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same field. Same data. Completely different execution plans. Check your &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; — if you see a full table scan where you expected an index range scan, type mismatch is the first thing to investigate.&lt;/p&gt;

&lt;p&gt;You can also create &lt;strong&gt;composite indexes&lt;/strong&gt; across multiple JSON fields, just like relational composite indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_cust_date&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderDate'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This powers queries that filter on customer and sort by date — a common API pattern — without touching the table at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Extended Types: What OSON Actually Stores
&lt;/h2&gt;

&lt;p&gt;Here's the thing that surprises developers: &lt;strong&gt;OSON stores more than the JSON spec defines.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The JSON standard has six types — string, number, boolean, null, object, array. That's the entire type system. Dates are strings. Timestamps are strings. High-precision decimals are floats (with all the IEEE 754 rounding baggage that implies). Binary data? Base64-encoded strings. Vectors? Not a thing.&lt;/p&gt;

&lt;p&gt;OSON is a superset. It stores Oracle's native scalar types &lt;strong&gt;directly in the document&lt;/strong&gt; — as themselves, not as stringified approximations. Write a &lt;code&gt;DATE&lt;/code&gt;, store a &lt;code&gt;DATE&lt;/code&gt;, read a &lt;code&gt;DATE&lt;/code&gt;. No parsing on the way in or out.&lt;/p&gt;

&lt;p&gt;This is why dot-notation has so many item methods and &lt;code&gt;JSON_VALUE&lt;/code&gt; has so many &lt;code&gt;RETURNING&lt;/code&gt; options. They're not type conversions — they're accessors for types that are already there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Extended Type Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OSON Type&lt;/th&gt;
&lt;th&gt;Dot-notation&lt;/th&gt;
&lt;th&gt;JSON_VALUE RETURNING&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VARCHAR2&lt;/code&gt; / text&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.string()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING VARCHAR2(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text, identifiers, categorical data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NUMBER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.number()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING NUMBER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Money, quantities, anything where rounding matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BINARY_DOUBLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.double()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING BINARY_DOUBLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IEEE 754 double — scientific math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BINARY_FLOAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.float()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING BINARY_FLOAT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single-precision float&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BOOLEAN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.boolean()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RETURNING BOOLEAN&lt;/code&gt; (23ai+)&lt;/td&gt;
&lt;td&gt;True/false flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.date()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING DATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Second-precision date+time (see note below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TIMESTAMP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.timestamp()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING TIMESTAMP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sub-second precision, no timezone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TIMESTAMP WITH TIME ZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.timestamp()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING TIMESTAMP WITH TIME ZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone-aware moments in time (see note below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERVAL YEAR TO MONTH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING INTERVAL YEAR TO MONTH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Month/year durations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERVAL DAY TO SECOND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING INTERVAL DAY TO SECOND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sub-second durations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RAW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.binary()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING RAW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hashes, fingerprints, binary payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VECTOR&lt;/code&gt; (26ai)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RETURNING VECTOR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Embeddings for similarity search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on &lt;code&gt;DATE&lt;/code&gt; and temporal types.&lt;/strong&gt; Oracle's &lt;code&gt;DATE&lt;/code&gt; despite the name actually stores &lt;em&gt;date + time down to the second&lt;/em&gt; — year, month, day, hour, minute, second. There's no "date without time" primitive in Oracle. If you want day-level matching (e.g., "all orders placed on April 11" regardless of the time of day), you'll want to &lt;code&gt;TRUNC()&lt;/code&gt; the value or use a range predicate like &lt;code&gt;WHERE dt &amp;gt;= DATE '2026-04-11' AND dt &amp;lt; DATE '2026-04-12'&lt;/code&gt;. &lt;code&gt;TIMESTAMP WITH TIME ZONE&lt;/code&gt; has the opposite subtlety — the same moment in time can have multiple representations (&lt;code&gt;2026-04-11 14:30 UTC&lt;/code&gt; == &lt;code&gt;2026-04-11 10:30 EDT&lt;/code&gt;), so equality comparisons need to either normalize to UTC or use the right comparison operator. These aren't OSON quirks — they're inherited from Oracle's core type system. The important thing is that OSON stores them &lt;strong&gt;as those types&lt;/strong&gt;, with full fidelity, so you get the same semantics as a relational column of the same type.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why You Care
&lt;/h3&gt;

&lt;p&gt;Dates are the feature developers underestimate until trouble tickets start piling up. In a text-based JSON store, your &lt;code&gt;orderDate&lt;/code&gt; is a string — and that string can be anything. Someone stores &lt;code&gt;"2026-04-11"&lt;/code&gt;, someone else stores &lt;code&gt;"2026-04-11T14:30:00Z"&lt;/code&gt;, a third person stores &lt;code&gt;"04/11/2026"&lt;/code&gt; because their frontend didn't normalize. Your sort order is now a lottery. Your range filter silently skips rows. Your index is polluted with mismatched formats that the database has no way to validate.&lt;/p&gt;

&lt;p&gt;So you push validation into application code. Every write path has to sanitize the date format before it hits the database. Every integration has to agree on the convention. Every bug report starts with "wait, what format is this field actually in?"&lt;/p&gt;

&lt;p&gt;In OSON, your &lt;code&gt;orderDate&lt;/code&gt; is a &lt;code&gt;DATE&lt;/code&gt;. Period. The database enforces it at insert time — anything that isn't a valid date gets rejected before it pollutes storage. Queries just work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Native date comparison. No parse. No cast. Index still works.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSDATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same logic applies to every extended type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NUMBER&lt;/code&gt;&lt;/strong&gt; — decimal precision IEEE 754 can't represent. Critical for money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TIMESTAMP WITH TIME ZONE&lt;/code&gt;&lt;/strong&gt; — scheduling across regions without reinventing timezone math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VECTOR&lt;/code&gt;&lt;/strong&gt; — similarity search in the same document as your operational data. No sidecar. No sync lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RAW&lt;/code&gt;&lt;/strong&gt; — binary hashes without base64 inflation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You stop writing parsing code. The database handles it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Typed Values Into OSON
&lt;/h3&gt;

&lt;p&gt;Three paths, depending on where the data comes from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. SQL/PL/SQL construction — automatic.&lt;/strong&gt; &lt;code&gt;JSON_OBJECT&lt;/code&gt; and &lt;code&gt;JSON_ARRAY&lt;/code&gt; preserve native types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'name'&lt;/span&gt;      &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="s1"&gt;'Product Launch'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'scheduled'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-15 09:30:00 America/New_York'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'price'&lt;/span&gt;     &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="mi"&gt;299&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- stored as NUMBER&lt;/span&gt;
  &lt;span class="s1"&gt;'confirmed'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;        &lt;span class="c1"&gt;-- stored as BOOLEAN&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;JSON_SCALAR&lt;/code&gt; — explicit.&lt;/strong&gt; Force a specific SQL type into a JSON scalar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'expiresAt'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;JSON_SCALAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30'&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Extended JSON syntax — the text path.&lt;/strong&gt; When JSON arrives as a string from an API, use type markers with the &lt;code&gt;EXTENDED&lt;/code&gt; keyword:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{
  "name": "Product Launch",
  "scheduled": {"$oracleTimestampTZ": "2026-06-15T09:30:00-04:00"}
}'&lt;/span&gt; &lt;span class="n"&gt;EXTENDED&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oracle recognizes &lt;code&gt;$oracleDate&lt;/code&gt;, &lt;code&gt;$oracleTimestamp&lt;/code&gt;, &lt;code&gt;$oracleTimestampTZ&lt;/code&gt;, &lt;code&gt;$oracleBinary&lt;/code&gt; and stores them as native types.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Payoff
&lt;/h3&gt;

&lt;p&gt;OSON makes JSON a structured document with first-class types — the type system in your database matches the type system in your application. No translation layer. No parsing code. No rounding bugs you find in production. Dates stay dates, numbers stay numbers, vectors stay vectors, all the way from storage to your response body.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. JSON_QUERY: When You Need the Whole Object
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;JSON_VALUE&lt;/code&gt; returns scalars. &lt;code&gt;JSON_QUERY&lt;/code&gt; returns JSON fragments — objects, arrays, or multiple values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Extract the shipping object (PRETTY needs a textual RETURNING type)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.shipping'&lt;/span&gt;
                  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PRETTY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;shipping_info&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Extract the items array (returns JSON by default)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;all_items&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Extract all product names (multiple values → need a wrapper)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*].product'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;WRAPPER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;product_names&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: ["Widget Pro","Gadget Plus","Cable Kit"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Wrapper Clause
&lt;/h3&gt;

&lt;p&gt;This is the part that confuses people. Here's the rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the path returns&lt;/th&gt;
&lt;th&gt;WITHOUT WRAPPER (default)&lt;/th&gt;
&lt;th&gt;WITH WRAPPER&lt;/th&gt;
&lt;th&gt;WITH CONDITIONAL WRAPPER&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single object/array&lt;/td&gt;
&lt;td&gt;Returns as-is&lt;/td&gt;
&lt;td&gt;Wraps in array&lt;/td&gt;
&lt;td&gt;Returns as-is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single scalar&lt;/td&gt;
&lt;td&gt;Error/NULL&lt;/td&gt;
&lt;td&gt;Wraps in array &lt;code&gt;[42]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Wraps in array &lt;code&gt;[42]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple values&lt;/td&gt;
&lt;td&gt;Error/NULL&lt;/td&gt;
&lt;td&gt;Wraps in array &lt;code&gt;[1,2,3]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Wraps in array &lt;code&gt;[1,2,3]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;WITH CONDITIONAL WRAPPER&lt;/code&gt; is the pragmatic choice — it wraps only when the result isn't already a single JSON value. Use it when you're not sure whether a path will return one thing or many.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error handling&lt;/strong&gt; in JSON_QUERY offers additional options beyond the &lt;code&gt;NULL&lt;/code&gt;/&lt;code&gt;ERROR&lt;/code&gt; pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Return empty array when path doesn't match&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.reviews'&lt;/span&gt; &lt;span class="n"&gt;EMPTY&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: []&lt;/span&gt;

&lt;span class="c1"&gt;-- Return empty object&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.metadata'&lt;/span&gt; &lt;span class="n"&gt;EMPTY&lt;/span&gt; &lt;span class="k"&gt;OBJECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: {}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are Oracle-specific. No other database gives you &lt;code&gt;EMPTY ARRAY ON ERROR&lt;/code&gt;. Sounds small — until you're building a JSON API response and don't want null checks littering your application code.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. JSON_EXISTS: Filtering with Path Predicates
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;JSON_EXISTS&lt;/code&gt; isn't a function — it's a SQL condition. It returns TRUE or FALSE. Use it in WHERE clauses to filter rows based on JSON content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Orders that have a shipping address&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.shipping.address'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Orders with at least one item over $25&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]?(@.unitPrice &amp;gt; 25)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;?(@.unitPrice &amp;gt; 25)&lt;/code&gt; is a &lt;strong&gt;filter expression&lt;/strong&gt;. The &lt;code&gt;@&lt;/code&gt; symbol refers to the current element being evaluated by the filter — which for &lt;code&gt;$.items[*]&lt;/code&gt; means each item in the array, one at a time. You can combine multiple conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Items that are expensive AND high quantity&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'$.items[*]?(@.unitPrice &amp;gt; 25 &amp;amp;&amp;amp; @.quantity &amp;gt;= 5)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Orders with specific tags&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'$.tags[*]?(@.string() == "wholesale")'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Nested existence checks&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="s1"&gt;'$?(@.status == "shipped"
             &amp;amp;&amp;amp; exists(@.items[*]?(@.category == "electronics")))'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Filters aren't just for arrays.&lt;/strong&gt; A filter is a predicate that applies to whatever path step it's attached to — the step doesn't have to return an array. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Return the customer name only when the customer lives in Belmont&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="s1"&gt;'$.customer?(@.address.city == "Belmont").name'&lt;/span&gt;
                  &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;belmont_customer&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no array in that path. &lt;code&gt;$.customer&lt;/code&gt; is a single object, and the filter either passes (returning the name) or fails (returning NULL). When used with &lt;code&gt;JSON_VALUE&lt;/code&gt;, non-matching rows come back as NULL — which is usually exactly what you want for "give me the name only if..." queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path expressions transparently process intermediate arrays.&lt;/strong&gt; This is a subtlety that catches people off guard, especially if you're coming from a language where arrays require explicit iteration. In Oracle's path expression engine, &lt;code&gt;$.a.b&lt;/code&gt; doesn't just navigate from field &lt;code&gt;a&lt;/code&gt; to field &lt;code&gt;b&lt;/code&gt; — if &lt;code&gt;a&lt;/code&gt; happens to be an array, the engine automatically iterates through every element and evaluates &lt;code&gt;.b&lt;/code&gt; on each one. You don't need &lt;code&gt;$.a[*].b&lt;/code&gt; — the &lt;code&gt;[*]&lt;/code&gt; is implicit.&lt;/p&gt;

&lt;p&gt;This matters most with &lt;code&gt;JSON_EXISTS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Works even if 'address' is an array of objects&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.shipping.address?(@.city == "Boston")'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;addresses&lt;/code&gt; is a single object, the filter tests that object. If &lt;code&gt;addresses&lt;/code&gt; is an array, the filter tests &lt;em&gt;each element&lt;/em&gt; — and returns true if any element matches. Same path expression, both shapes. You don't have to know (or care) whether the field is a scalar, an object, or an array when you write the filter. The engine handles the polymorphism.&lt;/p&gt;

&lt;p&gt;This is particularly useful for schemas that evolve over time — a field that started as a single value and later became an array doesn't break your existing queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter-then-project is a common pattern.&lt;/strong&gt; You can chain a filter with a projection to return a subset of fields from matching elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Get the SKUs of line items that cost more than $25&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_QUERY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'$.items[*]?(@.unitPrice &amp;gt; 25).product'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;WRAPPER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;expensive_skus&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ["Widget Pro","Gadget Plus"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;WITH WRAPPER&lt;/code&gt; clause. When a filter-then-project returns multiple values — like the SKUs above — you need &lt;code&gt;JSON_QUERY&lt;/code&gt; to re-wrap them into a JSON array. &lt;code&gt;JSON_VALUE&lt;/code&gt; won't work here because it only returns single scalars. This is the most common reason developers reach for &lt;code&gt;JSON_QUERY&lt;/code&gt; over &lt;code&gt;JSON_VALUE&lt;/code&gt; in real applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PASSING Clause: Bind Variables in Path Expressions
&lt;/h3&gt;

&lt;p&gt;This is a feature most developers don't know exists — and it changes how you write dynamic JSON filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'$.items[*]?(@.category == $cat &amp;amp;&amp;amp; @.unitPrice &amp;lt; $max)'&lt;/span&gt;
  &lt;span class="n"&gt;PASSING&lt;/span&gt; &lt;span class="s1"&gt;'electronics'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"cat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No string concatenation. No SQL injection risk. Bind variables plugged directly into the JSON path expression.&lt;/p&gt;

&lt;p&gt;This matters more than most developers realize. In document databases, dynamic filters typically mean building query strings in application code — concatenating user input into &lt;code&gt;$match&lt;/code&gt; pipelines or &lt;code&gt;find()&lt;/code&gt; predicates. Every concatenated string is a potential injection vector. Every dynamically constructed query is a unique query shape the engine has to parse and plan from scratch.&lt;/p&gt;

&lt;p&gt;The PASSING clause eliminates both problems at once. The CBO parses the path expression once, builds an execution plan once, and reuses it across every parameter combination. Ten thousand different category/price filter combinations hit the same optimized plan. That's not just safer — it's measurably faster. Hard-parsed queries burn CPU. Soft-parsed queries with bind variables don't. At scale, that difference shows up on your cloud bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full Filter Predicate Toolkit
&lt;/h3&gt;

&lt;p&gt;Oracle's path expressions support far more than basic comparisons:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;==&lt;/code&gt;, &lt;code&gt;!=&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;lt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?(@.price &amp;gt; 100)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; (AND), `\&lt;/td&gt;
&lt;td&gt;\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;exists()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$?exists(@.items[*]?(@.flagged == true))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;in()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?(@.category in ("electronics","tools"))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;has substring&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?(@.name has substring "Pro")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;starts with&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?(@.sku starts with "WDG-")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;like_regex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?(@.email like_regex ".*@oracle\\.com")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PostgreSQL users: this is roughly equivalent to &lt;code&gt;jsonb_path_exists&lt;/code&gt;, but richer. &lt;br&gt;&lt;br&gt;
MongoDB users: this is your &lt;code&gt;$elemMatch&lt;/code&gt; and &lt;code&gt;$regex&lt;/code&gt; — except it runs inside the SQL optimizer with access to indexes, join reordering, and cost-based plan selection.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. JSON_TABLE: The Bridge Between Worlds
&lt;/h2&gt;

&lt;p&gt;If you learn one SQL/JSON function from this article, make it &lt;code&gt;JSON_TABLE&lt;/code&gt;. It projects JSON data into relational rows and columns — the bridge between document flexibility and relational power.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;row_num&lt;/span&gt;    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;         &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;         &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;category&lt;/span&gt;   &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.category'&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ROW_NUM&lt;/th&gt;
&lt;th&gt;PRODUCT&lt;/th&gt;
&lt;th&gt;QUANTITY&lt;/th&gt;
&lt;th&gt;UNIT_PRICE&lt;/th&gt;
&lt;th&gt;CATEGORY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget Pro&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;29.99&lt;/td&gt;
&lt;td&gt;hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Gadget Plus&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;49.99&lt;/td&gt;
&lt;td&gt;electronics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Cable Kit&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4.99&lt;/td&gt;
&lt;td&gt;accessories&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each array element becomes a row. Each JSON field becomes a column. The &lt;code&gt;FOR ORDINALITY&lt;/code&gt; column auto-generates row numbers. And now you can do everything SQL does: &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;code&gt;SUM()&lt;/code&gt;, &lt;code&gt;AVG()&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt; to other tables, window functions — the full relational toolkit applied to JSON data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Column Types
&lt;/h3&gt;

&lt;p&gt;JSON_TABLE supports four column types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
         &lt;span class="c1"&gt;-- Value column (like JSON_VALUE): extracts a scalar&lt;/span&gt;
         &lt;span class="n"&gt;customer&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

         &lt;span class="c1"&gt;-- Query column (like JSON_QUERY): extracts JSON fragment&lt;/span&gt;
         &lt;span class="n"&gt;shipping&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.shipping'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

         &lt;span class="c1"&gt;-- Exists column (like JSON_EXISTS): returns 'true'/'false'&lt;/span&gt;
         &lt;span class="n"&gt;has_priority&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.priority'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

         &lt;span class="c1"&gt;-- Ordinality column: auto-generated row number&lt;/span&gt;
         &lt;span class="n"&gt;row_num&lt;/span&gt;     &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt;
       &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  NESTED PATH: Hierarchical Flattening
&lt;/h3&gt;

&lt;p&gt;This is where &lt;code&gt;JSON_TABLE&lt;/code&gt; gets powerful. Real-world JSON is nested. Orders contain items. Items contain variants. You need to flatten multiple levels without writing self-joins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;order_id&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;customer&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;item_num&lt;/span&gt;   &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what that produces from our sample order document:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ORDER_ID&lt;/th&gt;
&lt;th&gt;CUSTOMER&lt;/th&gt;
&lt;th&gt;ITEM_NUM&lt;/th&gt;
&lt;th&gt;PRODUCT&lt;/th&gt;
&lt;th&gt;QUANTITY&lt;/th&gt;
&lt;th&gt;UNIT_PRICE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Acme Corp&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Widget Pro&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;29.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Acme Corp&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Gadget Plus&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;49.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Acme Corp&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Cable Kit&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;4.99&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One JSON document became three relational rows. The parent fields (&lt;code&gt;order_id&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;) repeat for each array element. The &lt;code&gt;ITEM_NUM&lt;/code&gt; ordinality column tracks position within the array. This is the document-to-relational bridge in action — and from here, you can &lt;code&gt;GROUP BY category&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt; to a products table, run window functions, or anything else SQL gives you.&lt;/p&gt;

&lt;p&gt;The NESTED PATH clause creates an implicit lateral join — Oracle automatically cross-applies the nested array to the parent fields. Parent rows are preserved even when the array is empty (left outer join semantics). Sibling NESTED PATHs at the same level produce a union join, not a Cartesian product — Oracle knows you don't want row explosion.&lt;/p&gt;

&lt;p&gt;You can nest multiple levels deep, and you can have &lt;strong&gt;sibling&lt;/strong&gt; NESTED PATHs at the same level. This is where the join semantics get interesting. Consider a document with both &lt;code&gt;items&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;order_id&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;
           &lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.tags[*]'&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;tag&lt;/span&gt;        &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ORDER_ID&lt;/th&gt;
&lt;th&gt;PRODUCT&lt;/th&gt;
&lt;th&gt;QUANTITY&lt;/th&gt;
&lt;th&gt;TAG&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Widget Pro&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Gadget Plus&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Cable Kit&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;wholesale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Q1-promo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sibling NESTED PATHs produce a &lt;strong&gt;union join&lt;/strong&gt;, not a Cartesian product. You get 3 item rows + 3 tag rows = 6 total rows, not 3 × 3 = 9. Oracle fills the non-matching columns with NULLs. This is critical — without union join semantics, sibling arrays would cause row explosion that scales multiplicatively with array size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compare with MongoDB's &lt;code&gt;$unwind&lt;/code&gt;.&lt;/strong&gt; MongoDB's equivalent operation is the aggregation pipeline's &lt;code&gt;$unwind&lt;/code&gt; stage, which flattens one array at a time. To flatten both &lt;code&gt;items&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt;, you'd write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$unwind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$unwind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$tags&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a &lt;strong&gt;Cartesian product&lt;/strong&gt; — &lt;code&gt;|items| × |tags|&lt;/code&gt; rows per document. For our three-item, three-tag example, that's 9 rows per order, not 6. Now imagine a document with 50 line items and 20 tags: MongoDB gives you 1,000 rows. Oracle gives you 70. The ratio gets worse as the arrays grow.&lt;/p&gt;

&lt;p&gt;You can work around this in MongoDB with &lt;code&gt;$facet&lt;/code&gt; (which runs the two unwinds as independent sub-pipelines and merges the results), but now you're hand-engineering join semantics that Oracle handles automatically as part of the core &lt;code&gt;JSON_TABLE&lt;/code&gt; operator. One more case where the developer is the optimizer.&lt;/p&gt;

&lt;p&gt;You can also nest paths within paths for multi-level hierarchies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt; &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;product&lt;/span&gt;   &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;quantity&lt;/span&gt;  &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;item_ord&lt;/span&gt;  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ORDINALITY&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why JSON_TABLE Matters
&lt;/h3&gt;

&lt;p&gt;Here's the thing most developers miss: &lt;code&gt;JSON_TABLE&lt;/code&gt; doesn't just make JSON queryable. It makes JSON &lt;strong&gt;optimizable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once JSON is projected into relational columns, the CBO treats it like any other relational data. It can push predicates through the projection. It can use functional indexes. It can choose between nested loops, hash joins, and merge joins. It can parallelize.&lt;/p&gt;

&lt;p&gt;The document database world doesn't have this. MongoDB's aggregation pipeline is a sequential chain of stages — each stage processes the full result set of the previous stage. There's no cost-based optimizer reordering stages, pushing filters earlier, or choosing between join algorithms.&lt;/p&gt;

&lt;p&gt;What does that actually mean for you as a developer? It means you stop writing optimization logic in your application code.&lt;/p&gt;

&lt;p&gt;In a document database, you learn — usually the hard way — that the order you filter, sort, and aggregate matters. You hand-tune your aggregation pipeline stages. You restructure queries because putting &lt;code&gt;$match&lt;/code&gt; before &lt;code&gt;$unwind&lt;/code&gt; is faster than after. You read blog posts about which pipeline stages can use indexes and which can't. You become the optimizer. That's not your job.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;JSON_TABLE&lt;/code&gt;, you describe &lt;strong&gt;what you want&lt;/strong&gt; and the database figures out &lt;strong&gt;how to get it&lt;/strong&gt;. You write &lt;code&gt;WHERE category = 'electronics'&lt;/code&gt; on the outer query, and the CBO decides — without you asking — to push that filter down into the JSON scan, hit the functional index, and skip 99% of the documents. You join the flattened JSON to a products table, and the CBO picks the join algorithm (nested loops for small results, hash join for large ones) based on actual table statistics, not your guess about data volume.&lt;/p&gt;

&lt;p&gt;Add a column to the SELECT? The plan adapts. Data distribution changes as your table grows from 10K to 10M rows? The plan adapts. You didn't change a line of code. You write declarative SQL. The database does the engineering.&lt;/p&gt;

&lt;p&gt;Oracle's CBO has had 40+ years of development. When you use &lt;code&gt;JSON_TABLE&lt;/code&gt;, you're handing your JSON to that optimizer. That's not a small thing — it's the difference between writing query logic and writing query &lt;em&gt;infrastructure&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What's a CTE and Why Should You Care?
&lt;/h2&gt;

&lt;p&gt;A Common Table Expression (CTE) is a named, temporary result set defined in a &lt;code&gt;WITH&lt;/code&gt; clause that exists for the duration of a single SQL statement. If you've never used one, think of it as a named subquery that you can reference multiple times — and that makes complex SQL readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;high_value_items&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
           &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;         &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;         &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;line_total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;high_value_items&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;line_total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a CTE. The &lt;code&gt;WITH&lt;/code&gt; clause defines &lt;code&gt;high_value_items&lt;/code&gt;. The main query uses it like a table. The query reads top-to-bottom, each step building on the last.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why CTEs Matter for JSON Work
&lt;/h3&gt;

&lt;p&gt;JSON transformations are inherently multi-step: flatten the document, filter and enrich, then re-assemble into a new shape. Without CTEs, you're nesting subqueries five levels deep. With CTEs, each step is a named, testable, readable block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="c1"&gt;-- Step 1: Flatten JSON into rows&lt;/span&gt;
  &lt;span class="n"&gt;raw_items&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
             &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;category&lt;/span&gt;   &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.category'&lt;/span&gt;
             &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;-- Step 2: Join with relational product catalog&lt;/span&gt;
  &lt;span class="n"&gt;enriched_items&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;raw_items&lt;/span&gt; &lt;span class="n"&gt;ri&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;-- Step 3: Aggregate by category&lt;/span&gt;
  &lt;span class="n"&gt;category_totals&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;enriched_items&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 4: Build the JSON response&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'revenue'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;category_totals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four steps. Each one readable in isolation. Each one debuggable by replacing the final SELECT with &lt;code&gt;SELECT * FROM step_N&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple CTEs and Chaining
&lt;/h3&gt;

&lt;p&gt;CTEs can reference earlier CTEs in the same &lt;code&gt;WITH&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;step1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;step2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'A%'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;step3&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step2&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Acme%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CTEs vs Aggregation Pipelines: A Mental Model
&lt;/h3&gt;

&lt;p&gt;If you're coming from MongoDB, you already understand multi-step data transformations — that's what the aggregation pipeline is. CTEs are the same idea, with two critical differences: &lt;strong&gt;named addressability&lt;/strong&gt; and &lt;strong&gt;optimizer-driven execution order&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A MongoDB pipeline is an array of stages that execute in declaration order. Modern pipelines &lt;em&gt;can&lt;/em&gt; branch via &lt;code&gt;$facet&lt;/code&gt;, &lt;code&gt;$lookup&lt;/code&gt;, and &lt;code&gt;$unionWith&lt;/code&gt;, so technically the pipeline is a DAG too — but stages still run in the order you wrote them, with only limited pipelining across non-blocking stages. There's no cost-based reordering. There's no "the optimizer decided it was cheaper to filter before the unwind." If you put &lt;code&gt;$match&lt;/code&gt; after &lt;code&gt;$unwind&lt;/code&gt; when it should have been before, you pay for it — and so does every row that had to be flattened just to get thrown away a stage later.&lt;/p&gt;

&lt;p&gt;And here's a detail that matters more than it looks: &lt;code&gt;$facet&lt;/code&gt; branches execute &lt;strong&gt;sequentially&lt;/strong&gt;, not in parallel. Each sub-pipeline runs to completion before the next one starts. You get the &lt;em&gt;shape&lt;/em&gt; of a branch — separate result sets from the same input — but not the &lt;em&gt;physics&lt;/em&gt; of parallelism. If Branch A takes 200ms and Branch B takes 300ms, you wait 500ms, not 300ms. The branches share an input document set, but the engine processes them one at a time.&lt;/p&gt;

&lt;p&gt;CTEs are also a DAG, but with three key differences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Each step is named and addressable.&lt;/strong&gt; The final SELECT can reach into &lt;em&gt;any&lt;/em&gt; CTE simultaneously — join &lt;code&gt;raw_items&lt;/code&gt; to &lt;code&gt;category_totals&lt;/code&gt;, filter on &lt;code&gt;enriched&lt;/code&gt;, aggregate across all three. You're not locked into the most-recent-stage-feeds-the-next-stage structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CBO decides the execution order.&lt;/strong&gt; Push a predicate from the outer query into the innermost CTE? Sure. Reorder joins based on row estimates? Yes. Pick a hash join here and a nested loop there? Automatic. You describe the logic; the optimizer decides the physics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent branches can execute in parallel.&lt;/strong&gt; When the CBO sees that two CTEs have no data dependency on each other, it can run them concurrently. The same two branches that take 500ms sequentially in &lt;code&gt;$facet&lt;/code&gt; take 300ms under Oracle's parallel execution — because the optimizer knows they're independent and schedules them accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil86stgz3zxtoyca4s72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fil86stgz3zxtoyca4s72.png" alt=" " width="800" height="740"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The differences compound in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Aggregation Pipeline&lt;/th&gt;
&lt;th&gt;CTEs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anonymous (position in array)&lt;/td&gt;
&lt;td&gt;Named (referenced by name)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mostly linear; branches via &lt;code&gt;$facet&lt;/code&gt;/&lt;code&gt;$lookup&lt;/code&gt;/&lt;code&gt;$unionWith&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full DAG: any step can reference any earlier step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution order&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs in declaration order, no cost-based reordering&lt;/td&gt;
&lt;td&gt;CBO reorders based on statistics and predicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$facet&lt;/code&gt; branches execute sequentially, not in parallel&lt;/td&gt;
&lt;td&gt;CBO parallelizes independent branches automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Insert &lt;code&gt;$out&lt;/code&gt; or &lt;code&gt;explain()&lt;/code&gt; at specific stages&lt;/td&gt;
&lt;td&gt;Replace final SELECT with &lt;code&gt;SELECT * FROM step_N&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reuse&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can't reference earlier stages from later ones (except via &lt;code&gt;$facet&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any CTE reusable by multiple downstream steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer hand-tunes stage order&lt;/td&gt;
&lt;td&gt;CBO pushes predicates, picks join methods, parallelizes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The aggregation pipeline forces you to think about &lt;em&gt;how&lt;/em&gt; the database should process data — stage order matters, &lt;code&gt;$match&lt;/code&gt; belongs before &lt;code&gt;$unwind&lt;/code&gt;, put your projections last to minimize intermediate document size. CTEs let you think about &lt;em&gt;what&lt;/em&gt; the data should look like at each logical stage, and the CBO figures out the how. That's the core difference: &lt;strong&gt;you become the optimizer in one model, and you stop being the optimizer in the other.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursive CTEs
&lt;/h3&gt;

&lt;p&gt;For hierarchical data — org charts, category trees, bill-of-materials — Oracle supports recursive CTEs. The structure has two parts: an &lt;strong&gt;anchor&lt;/strong&gt; query that selects the starting rows, and a &lt;strong&gt;recursive&lt;/strong&gt; query that joins back to the CTE itself to walk the hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;category_tree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lvl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- Anchor: start at root categories (no parent)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;categories&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="c1"&gt;-- Recursive: for each row found so far, find its children&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lvl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;category_tree&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="n"&gt;DEPTH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lvl&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;category_hierarchy&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;category_tree&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SEARCH&lt;/code&gt; clause controls what order the recursion visits nodes, and generates a &lt;code&gt;seq&lt;/code&gt; column you can use for deterministic &lt;code&gt;ORDER BY&lt;/code&gt;. Think of it like walking a tree — you have two choices for how to walk it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SEARCH DEPTH FIRST BY name&lt;/code&gt;&lt;/strong&gt; — go deep before going wide. Visit a node, then immediately visit its children, then their children, all the way to the leaf, before backtracking to visit siblings. This produces the indented tree output you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Electronics
  Laptops
    Gaming Laptops
    Ultrabooks
  Phones
    Android
    iOS
Clothing
  Men
  Women
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;SEARCH BREADTH FIRST BY name&lt;/code&gt;&lt;/strong&gt; — go wide before going deep. Visit all nodes at level 1, then all nodes at level 2, then level 3. This produces a level-by-level listing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Electronics
Clothing
  Laptops
  Phones
  Men
  Women
    Gaming Laptops
    Ultrabooks
    Android
    iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depth-first is what you want for tree displays, navigation menus, and indented reports. Breadth-first is useful when you care about levels — "show me all second-level categories" or "find everything within two hops of this node."&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BY name&lt;/code&gt; part determines the sort order among siblings at each level. Replace &lt;code&gt;name&lt;/code&gt; with any column — &lt;code&gt;BY created_date&lt;/code&gt; visits the oldest siblings first, &lt;code&gt;BY priority DESC&lt;/code&gt; visits the highest priority first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cycle detection:&lt;/strong&gt; Hierarchical data can have bugs — a category that's its own grandparent creates an infinite loop. Oracle catches this with the &lt;code&gt;CYCLE&lt;/code&gt; clause on the recursive CTE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;category_tree&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lvl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lvl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;category_tree&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CYCLE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'N'&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lvl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_cycle&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;category_tree&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds an &lt;code&gt;is_cycle&lt;/code&gt; column and stops recursion on any row that would revisit an already-seen &lt;code&gt;id&lt;/code&gt;. Without it, a cyclic reference means a runaway query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Materialization: When Performance Matters
&lt;/h3&gt;

&lt;p&gt;The CBO decides whether to &lt;strong&gt;materialize&lt;/strong&gt; a CTE (store results in a temp table) or &lt;strong&gt;inline&lt;/strong&gt; it (copy the SQL text into the main query). The default behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Referenced &lt;strong&gt;once&lt;/strong&gt;: the CBO inlines it (allows predicate pushing, index usage)&lt;/li&gt;
&lt;li&gt;Referenced &lt;strong&gt;multiple times&lt;/strong&gt;: the CBO materializes it (compute once, read many)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can override this with hints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;expensive_json_parse&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="cm"&gt;/*+ MATERIALIZE */&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderDate'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;expensive_json_parse&lt;/span&gt; &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to materialize:&lt;/strong&gt; The CTE does expensive work that's reused multiple times, especially when the CBO might otherwise inline and recompute. Compute it once, read it many.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to inline:&lt;/strong&gt; The main query has selective predicates that need to push down into the JSON scan to hit functional indexes. Inlining keeps the optimization boundary open.&lt;/p&gt;

&lt;p&gt;Here's a case where &lt;code&gt;MATERIALIZE&lt;/code&gt; is genuinely the right call — a self-join on an expensively-parsed CTE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cross-sell analysis: find customer pairs who both bought products&lt;/span&gt;
&lt;span class="c1"&gt;-- in the same category. The CTE is self-joined, so it's referenced twice.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;customer_categories&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="cm"&gt;/*+ MATERIALIZE */&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
           &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.category'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;shared_category&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;customer_categories&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;customer_categories&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
                            &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without materialization, the CBO might inline &lt;code&gt;customer_categories&lt;/code&gt; into both sides of the self-join — meaning the JSON_TABLE flattening runs &lt;strong&gt;twice&lt;/strong&gt;, parsing every order document twice. With &lt;code&gt;MATERIALIZE&lt;/code&gt;, the parse happens once, the result lands in a session-scoped temp table, and both sides of the self-join read from that temp table at memory speed.&lt;/p&gt;

&lt;p&gt;For a 10-million-row orders table with 5 items per document on average, that's the difference between 10 million JSON parses and 20 million. Same result. Half the work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on auto-materialization:&lt;/strong&gt; Oracle automatically materializes CTEs that are referenced multiple times — so in the example above, the CBO often makes this decision on its own. The explicit &lt;code&gt;/*+ MATERIALIZE */&lt;/code&gt; hint does two things: it documents intent (so future readers understand the query was designed with materialization in mind), and it protects against CBO regressions when statistics shift or plans change between database versions. For a CTE that's expensive to compute and referenced multiple times, making the hint explicit is defensive engineering.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The INLINE hint goes the other direction&lt;/strong&gt; — it tells Oracle to treat the CTE as if you had copy-pasted its SQL into the main query, instead of walling it off in a temp table. Why would you want that? Because materialization creates a boundary the optimizer can't see through. Once results are in a temp table, Oracle can't look at what the outer query is going to do with them — it has to compute the full CTE result, every row, before your &lt;code&gt;WHERE&lt;/code&gt; clause even runs.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;recent_orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="cm"&gt;/*+ INLINE */&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderDate'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;recent_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSDATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it like this: if the CTE is inlined, Oracle can see that you only want orders from the last 7 days. It looks at your functional index on &lt;code&gt;$.orderDate&lt;/code&gt;, jumps straight to those rows, and reads maybe 0.1% of the table.&lt;/p&gt;

&lt;p&gt;If the CTE is materialized, Oracle has to compute &lt;code&gt;recent_orders&lt;/code&gt; first — which means reading every order, parsing every JSON document, building the full intermediate result — and &lt;em&gt;then&lt;/em&gt; filtering to the last 7 days. On a 10-million-row table, that's parsing 10 million documents to return 10,000 rows. The index sits unused because the optimizer never gets a chance to use it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;INLINE&lt;/code&gt; hint removes the wall. It tells Oracle "don't stage this — fold it into the main query so you can see the whole picture and optimize across it." When your outer query has a selective filter that should hit an index, inlining is how you make sure that happens.&lt;/p&gt;

&lt;p&gt;Both hints exist for the same reason: sometimes you know the shape of your data better than the optimizer's statistics do. Check your execution plans with &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; or &lt;code&gt;DBMS_XPLAN&lt;/code&gt;. If you see a full table scan where you expected an index range scan, materialization is often the culprit — and &lt;code&gt;INLINE&lt;/code&gt; is the fix. If you see the same expensive CTE appearing multiple times in the plan, that's the opposite problem — and &lt;code&gt;MATERIALIZE&lt;/code&gt; is the fix.&lt;/p&gt;

&lt;p&gt;Hint only when the execution plan tells you to. Trust the CBO by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Building JSON: The Construction Functions
&lt;/h2&gt;

&lt;p&gt;Reading JSON is half the story. The other half is constructing JSON from relational data — building API responses, materializing document views, assembling complex payloads.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important: the default return type is VARCHAR2, not JSON.&lt;/strong&gt; &lt;code&gt;JSON_OBJECT&lt;/code&gt;, &lt;code&gt;JSON_ARRAY&lt;/code&gt;, &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt;, and &lt;code&gt;JSON_OBJECTAGG&lt;/code&gt; all default to &lt;code&gt;RETURNING VARCHAR2(4000)&lt;/code&gt; for backward compatibility. That means without an explicit &lt;code&gt;RETURNING JSON&lt;/code&gt; clause, you're getting a serialized text string — not the binary OSON representation. This causes two real problems: (1) when you nest one of these inside another, you have to use &lt;code&gt;FORMAT JSON&lt;/code&gt; to prevent double-escaping (more on this in a moment), and (2) you lose the type fidelity OSON gives you (numbers stay numbers, dates stay dates, etc.). &lt;strong&gt;Always add &lt;code&gt;RETURNING JSON&lt;/code&gt; when you mean to produce JSON-typed data&lt;/strong&gt;, especially in CTEs that feed downstream JSON construction. The examples in this section show both forms — explicit and implicit — so you can see how each behaves. In production code, default to &lt;code&gt;RETURNING JSON&lt;/code&gt; and only drop it when you specifically want a serialized text result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;JSON { }&lt;/code&gt; and &lt;code&gt;JSON [ ]&lt;/code&gt; shorthand (26ai).&lt;/strong&gt; Oracle added JSON-like constructor syntax that solves the &lt;code&gt;RETURNING&lt;/code&gt; problem by defaulting to &lt;code&gt;RETURNING JSON&lt;/code&gt; instead of &lt;code&gt;VARCHAR2&lt;/code&gt;. &lt;code&gt;JSON { 'foo' : 'bar' }&lt;/code&gt; is equivalent to &lt;code&gt;JSON_OBJECT('foo' : 'bar' RETURNING JSON)&lt;/code&gt;, and &lt;code&gt;JSON [ 1, 2, 3 ]&lt;/code&gt; is equivalent to &lt;code&gt;JSON_ARRAY(1, 2, 3 RETURNING JSON)&lt;/code&gt;. If you're writing new code on 26ai, prefer the &lt;code&gt;JSON { }&lt;/code&gt; form — it's shorter, the return type is correct by default, and you never have to worry about missing &lt;code&gt;RETURNING JSON&lt;/code&gt; or &lt;code&gt;FORMAT JSON&lt;/code&gt;. The &lt;code&gt;JSON_OBJECT&lt;/code&gt; / &lt;code&gt;JSON_ARRAY&lt;/code&gt; functions remain available and are the right choice when you need explicit &lt;code&gt;RETURNING VARCHAR2&lt;/code&gt; or &lt;code&gt;FORMAT JSON&lt;/code&gt; control.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  JSON_OBJECT: Rows to Objects
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Key-value pairs&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'customer'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="s1"&gt;'total'&lt;/span&gt;    &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
             &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_summary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- {"customer":"Acme Corp","total":799.4}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Column shorthand&lt;/strong&gt; — the column name becomes the key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- {"employee_id":100,"first_name":"Steven","last_name":"King","email":"SKING"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wildcard&lt;/strong&gt; — all columns at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NULL handling&lt;/strong&gt; — choose your API contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Include nulls explicitly (default)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'middle'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- {"name":"Alice","middle":null}&lt;/span&gt;

&lt;span class="c1"&gt;-- Omit nulls entirely&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="s1"&gt;'Alice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'middle'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="n"&gt;ABSENT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- {"name":"Alice"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JSON_ARRAY: Building Arrays
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What's &lt;code&gt;DUAL&lt;/code&gt;?&lt;/strong&gt; Think of it as Oracle's REPL. When you open a Node or Python shell and type &lt;code&gt;1 + 1&lt;/code&gt;, you get &lt;code&gt;2&lt;/code&gt; back — no variables, no setup, just the expression evaluated. &lt;code&gt;DUAL&lt;/code&gt; is how you do that in SQL. It's a built-in one-row table that exists purely as a target for expressions you want to run standalone. &lt;code&gt;SELECT SYSDATE FROM DUAL&lt;/code&gt; is the SQL equivalent of typing &lt;code&gt;Date.now()&lt;/code&gt; into a JavaScript console. Any time you see &lt;code&gt;FROM DUAL&lt;/code&gt; in this article, we're just running an expression to show you the result — treat the code block like a REPL session.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'two'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- [1,"two",3.0]   (NULL omitted by default — ABSENT ON NULL)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'two'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- [1,"two",3.0,null]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;26ai: Build JSON arrays directly from a query.&lt;/strong&gt; This is the syntax you want when you're building an API response. You have rows in a table. You need them as a JSON array in the response body. In 26ai, you just write that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;list_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;products&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;list_price&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;product_catalog&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it like you'd read code: "give me a JSON array containing an object per product in category 10, sorted by price." That's exactly what the SQL says. No extra steps.&lt;/p&gt;

&lt;p&gt;Before 26ai, that simple intent required extra SQL plumbing — you had to stage the rows into a helper query and pull them out with a different function. For anything beyond trivial API responses, this added real noise to your code. The 26ai version is what you'd write on a whiteboard if somebody asked you how it had to work under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON_ARRAYAGG and JSON_OBJECTAGG: Aggregation into JSON
&lt;/h3&gt;

&lt;p&gt;Here's the mental model: &lt;code&gt;JSON_OBJECT&lt;/code&gt; and &lt;code&gt;JSON_ARRAY&lt;/code&gt; build JSON from &lt;strong&gt;one row at a time&lt;/strong&gt;. &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt; and &lt;code&gt;JSON_OBJECTAGG&lt;/code&gt; build JSON from &lt;strong&gt;many rows at once&lt;/strong&gt;. Same as the difference between writing &lt;code&gt;row.toJSON()&lt;/code&gt; in your ORM versus calling &lt;code&gt;.map(r =&amp;gt; r.toJSON())&lt;/code&gt; over a result set — the singular version shapes one record, the plural version rolls up a collection.&lt;/p&gt;

&lt;p&gt;These are the workhorses of API response building. Any time you have a parent-child relationship — customer and their orders, order and its line items, post and its comments — you're going to use &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt;. It's the function that collapses "many rows" into "one JSON array" so the structure matches what your frontend or API consumer actually wants.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Roll up all items in an order into a ranked JSON array&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s1"&gt;'total'&lt;/span&gt;   &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;
              &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;items_ranked&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;RETURNING JSON&lt;/code&gt; on &lt;strong&gt;both&lt;/strong&gt; the inner &lt;code&gt;JSON_OBJECT&lt;/code&gt; and the outer &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt;. Without those, you'd be aggregating VARCHAR2 strings into a VARCHAR2 string, which means double-escaping issues and lost type fidelity. With them, you're aggregating JSON values into a JSON value — clean, binary, native.&lt;/p&gt;

&lt;p&gt;Read that as: "take every item row, shape each one as a &lt;code&gt;{product, total}&lt;/code&gt; object, then roll them up into a single array sorted by total descending." One statement does the row-level shaping and the collection-level rollup. No application code. No loops. No post-processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ORDER BY&lt;/code&gt; inside the aggregate&lt;/strong&gt; is important — it controls the order of elements in the resulting JSON array. Without it, array element order is undefined. With it, you can guarantee that your API consumers get data in the order they expect (top-N lists, chronological feeds, priority queues).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;JSON_OBJECTAGG&lt;/code&gt;&lt;/strong&gt; is the less common sibling — it builds a single JSON object where keys and values both come from table rows. Think of it as turning two columns into a dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Turn a settings table into a JSON config object&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECTAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;setting_name&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;setting_value&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;user_settings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- {"theme":"dark","language":"en","timezone":"America/Chicago"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it when you have key-value pair data in rows and need a lookup object in the response. Settings, feature flags, translation dictionaries, metadata maps — anything where you'd otherwise reduce rows into an object in application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why these are the "workhorses":&lt;/strong&gt; Every non-trivial API response needs nested collections. Every nested collection starts as rows in a table. &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt; and &lt;code&gt;JSON_OBJECTAGG&lt;/code&gt; are the bridge between those two worlds. Combined with &lt;code&gt;JSON_OBJECT&lt;/code&gt; for per-row shaping and the &lt;code&gt;FORMAT JSON&lt;/code&gt; clause for composition, they let you build arbitrarily nested API responses in a single SQL statement — no middleware, no serialization layer, no ORM-to-JSON mapping code.&lt;/p&gt;

&lt;p&gt;The database returns the JSON. Your handler just sends it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The FORMAT JSON Clause
&lt;/h3&gt;

&lt;p&gt;Here's the gotcha that catches every developer on day one. You build a JSON array in a CTE. You reference it in a &lt;code&gt;JSON_OBJECT&lt;/code&gt; to nest it inside a parent object. You run the query. You get back this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"orderItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;product&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Widget Pro&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;},{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;product&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Gadget Plus&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}]"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a nested array. That's a &lt;strong&gt;string&lt;/strong&gt; containing an escaped JSON array. Your frontend just received &lt;code&gt;"orderItems"&lt;/code&gt; as text — they'd have to call &lt;code&gt;JSON.parse()&lt;/code&gt; on the value to use it. Broken.&lt;/p&gt;

&lt;p&gt;There are two fixes — and the better one is what we just talked about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix #1 (preferred): use &lt;code&gt;RETURNING JSON&lt;/code&gt; so the intermediate result is JSON, not VARCHAR2.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;item_array&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;items_json&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
         &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'orderItems'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;items_json&lt;/span&gt;     &lt;span class="c1"&gt;-- Already JSON, no FORMAT JSON needed&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;item_array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;items_json&lt;/code&gt; is now a JSON-typed column. When you nest it inside the outer &lt;code&gt;JSON_OBJECT&lt;/code&gt;, Oracle knows it's already JSON and doesn't escape it. This is the cleanest pattern — and the one Zhen Hua Liu (the OSON architect) recommends — because it preserves type fidelity all the way through the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix #2 (legacy / interop): use &lt;code&gt;FORMAT JSON&lt;/code&gt; to vouch for a VARCHAR2 value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're working with a &lt;code&gt;VARCHAR2&lt;/code&gt; or &lt;code&gt;CLOB&lt;/code&gt; column that already contains serialized JSON — perhaps from an external source, a legacy table, or a function that returns text — you don't have the luxury of changing the source type. That's where &lt;code&gt;FORMAT JSON&lt;/code&gt; earns its keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'payload'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;legacy_text_column&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;   &lt;span class="c1"&gt;-- Trust me, this is JSON&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legacy_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FORMAT JSON&lt;/code&gt; tells Oracle "this string is already valid JSON, insert it as-is, don't escape it." Without that clause, Oracle assumes text — and text gets escaped for safety (otherwise a malicious value containing &lt;code&gt;"}&lt;/code&gt; could break out of the parent structure). With it, Oracle inserts the value verbatim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need &lt;code&gt;FORMAT JSON&lt;/code&gt;:&lt;/strong&gt; working with &lt;code&gt;VARCHAR2&lt;/code&gt;/&lt;code&gt;CLOB&lt;/code&gt; columns that store serialized JSON. Legacy tables. External payloads. Anywhere the source type is text but the content is JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you don't:&lt;/strong&gt; When you control the source type. Use &lt;code&gt;RETURNING JSON&lt;/code&gt; on every constructor in the pipeline and the problem disappears entirely.&lt;/p&gt;

&lt;p&gt;This trips up every developer exactly once — usually on a Monday morning, usually ten minutes before a demo. Now you know how to avoid it (use &lt;code&gt;RETURNING JSON&lt;/code&gt; everywhere) &lt;strong&gt;and&lt;/strong&gt; how to fix it when you can't (use &lt;code&gt;FORMAT JSON&lt;/code&gt; to vouch for the bytes).&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Transforming JSON In-Place
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't want to decompose and rebuild — you want to modify the JSON document directly. Oracle gives you two tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON_TRANSFORM: The Swiss Army Knife
&lt;/h3&gt;

&lt;p&gt;Here's the pattern every developer has written at least once: pull a document out of the database, &lt;code&gt;JSON.parse&lt;/code&gt; it, mutate a few fields in application code, &lt;code&gt;JSON.stringify&lt;/code&gt; it back, and write it to the database. Maybe wrap the whole thing in a read-modify-write loop with optimistic concurrency to avoid losing updates. Maybe get it wrong the first time. Probably write a retry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JSON_TRANSFORM&lt;/code&gt; is the SQL-native alternative. You tell Oracle what you want changed. Oracle does the mutation on the binary OSON structure in place — no parse, no serialize, no round-trip, no race. Introduced in 21c and dramatically expanded in 26ai, it's the most expressive in-place JSON modification function in any database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Multiple operations in a single call&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_TRANSFORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="s1"&gt;'$.status'&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'delivered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt;    &lt;span class="s1"&gt;'$.deliveredAt'&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;REMOVE&lt;/span&gt; &lt;span class="s1"&gt;'$.priority'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="s1"&gt;'$.customer'&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customerName'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;APPEND&lt;/span&gt; &lt;span class="s1"&gt;'$.tags'&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PRETTY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that like a script: update the status, stamp the delivery time, drop the priority field, rename &lt;code&gt;customer&lt;/code&gt; to &lt;code&gt;customerName&lt;/code&gt;, append &lt;code&gt;"completed"&lt;/code&gt; to the tags array. Five mutations. One function call. One atomic operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core operations:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Think of it like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create the field if missing, replace if present&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;obj[key] = value&lt;/code&gt; (JavaScript)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create only — error if the field already exists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dict.setdefault(key, value)&lt;/code&gt; (Python)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REPLACE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace only — no-op if the field is missing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if key in obj: obj[key] = value&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APPEND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Push a value onto a JSON array&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arr.push(value)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REMOVE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a field or array element&lt;/td&gt;
&lt;td&gt;&lt;code&gt;delete obj[key]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RENAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change a field name, keeping its value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;obj[newKey] = obj[oldKey]; delete obj[oldKey]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KEEP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whitelist: keep these fields, drop everything else&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pick(obj, [...])&lt;/code&gt; from Lodash&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each operation has its own error handling — you decide what should happen if the target path is missing, already exists, or has a NULL value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Only stamp createdAt if it's not already there (won't overwrite existing audits)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;order_doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JSON_TRANSFORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="s1"&gt;'$.audit.createdAt'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt;   &lt;span class="c1"&gt;-- create the path if missing&lt;/span&gt;
  &lt;span class="k"&gt;IGNORE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;EXISTING&lt;/span&gt;                          &lt;span class="c1"&gt;-- skip silently if already set&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Every one of these operations happens atomically inside a single SQL transaction. No lost updates. No torn writes. No "I parsed version 1, you parsed version 1, I wrote version 2, you wrote version 2, my change is gone." You skip the entire class of concurrency bugs that the parse-mutate-write pattern introduces — because there's no parse and no intermediate state for another session to race against.&lt;/p&gt;

&lt;p&gt;And because Oracle modifies the OSON binary format in place, updates are &lt;strong&gt;piecewise&lt;/strong&gt; — only the changed portions of the document get written to disk, undo, and redo. A 2KB update to a 2MB document costs 2KB of I/O, not 2MB. At scale, that's the difference between a blog-post database and a system of record.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conditional and Array Transformations
&lt;/h3&gt;

&lt;p&gt;Set a value on every element of an array using &lt;code&gt;[*]&lt;/code&gt; in the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Mark every item as tax-exempt&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_TRANSFORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*].taxExempt'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PRETTY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditional updates use a &lt;code&gt;CASE&lt;/code&gt; expression in plain SQL — set the value based on a &lt;code&gt;JSON_VALUE&lt;/code&gt; extraction, then write it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_TRANSFORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'$.trackable'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;
                        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;
                          &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
                        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
                      &lt;span class="k"&gt;END&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PRETTY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Array set operations: &lt;code&gt;UNION&lt;/code&gt;, &lt;code&gt;INTERSECT&lt;/code&gt;, &lt;code&gt;MINUS&lt;/code&gt;, &lt;code&gt;SORT&lt;/code&gt; — applied to JSON arrays like relational set operators.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON_MERGEPATCH: The Simple Alternative
&lt;/h3&gt;

&lt;p&gt;For straightforward updates, &lt;code&gt;JSON_MERGEPATCH&lt;/code&gt; implements RFC 7396:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Update status, add a field, remove a field (set to null)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;order_doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JSON_MERGEPATCH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{"status": "delivered", "trackingUrl": "https://track.example.com/1001", "priority": null}'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules are simple: new keys are added, existing keys are replaced, &lt;code&gt;null&lt;/code&gt; removes the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; You can't modify individual array elements. &lt;code&gt;JSON_MERGEPATCH&lt;/code&gt; replaces arrays wholesale. For granular array work, use &lt;code&gt;JSON_TRANSFORM&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Putting It All Together: The CTE Pipeline
&lt;/h2&gt;

&lt;p&gt;Here's where everything converges. Let's build a realistic query: flatten orders from JSON, join with a relational product catalog, compute analytics, and construct a new JSON API response. One statement. One transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
&lt;span class="c1"&gt;-- Step 1: Flatten JSON orders into relational rows&lt;/span&gt;
&lt;span class="n"&gt;order_lines&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderId'&lt;/span&gt;
                    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderDate'&lt;/span&gt;
                    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;jt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;JSON_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.items[*]'&lt;/span&gt;
           &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;product&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;quantity&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.quantity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;        &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.unitPrice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;category&lt;/span&gt;   &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.category'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;jt&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: Enrich with relational product catalog&lt;/span&gt;
&lt;span class="n"&gt;enriched&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;line_total&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;order_lines&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: Aggregate by category&lt;/span&gt;
&lt;span class="n"&gt;category_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;category_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;units_sold&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;enriched&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4: Construct the JSON API response&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'customer'&lt;/span&gt;   &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'categories'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'category'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'total'&lt;/span&gt;    &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'units'&lt;/span&gt;    &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;units_sold&lt;/span&gt;
      &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_total&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;api_response&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;category_summary&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it top to bottom. Each CTE is one logical step. Each step is testable in isolation (replace the final SELECT with &lt;code&gt;SELECT * FROM enriched&lt;/code&gt; to debug step 2). The CBO optimizes the entire pipeline as a single execution plan — it can push predicates from step 4 down into step 1, reorder joins, and choose the optimal access path for each table.&lt;/p&gt;

&lt;p&gt;Now stop and think about what just happened. You described the data you wanted. You described how to shape it. You handed that description to Oracle. What you got back was a fully-formed JSON API response — computed, aggregated, sorted, assembled — in one round trip, one transaction, one execution plan.&lt;/p&gt;

&lt;p&gt;Count the code that didn't have to exist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No ORM.&lt;/li&gt;
&lt;li&gt;No serialization layer.&lt;/li&gt;
&lt;li&gt;No hand-written JSON builder walking through result sets in application code.&lt;/li&gt;
&lt;li&gt;No stitching together results from multiple queries.&lt;/li&gt;
&lt;li&gt;No worrying about which query reads the latest data and which one doesn't.&lt;/li&gt;
&lt;li&gt;No retry logic when a mid-pipeline read returns stale state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handler code to deliver this API response is now one line: &lt;code&gt;res.send(row)&lt;/code&gt;. That's the whole request path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Multi-Model Query
&lt;/h3&gt;

&lt;p&gt;This is where the gloves come off. In Oracle 26ai, the pipeline extends to vector search, graph traversal, and spatial queries — all in the same &lt;code&gt;WITH&lt;/code&gt; clause, all in the same transaction, all under the same optimizer.&lt;/p&gt;

&lt;p&gt;To make the example self-contained, here's a minimal RAG-style schema with three docs, a vector embedding per doc, and a small knowledge graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;     &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;  &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;doc_id&lt;/span&gt;     &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_text&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt;  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;node_id&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;doc_id&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;      &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;kg_edges&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;edge_id&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;src_id&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dst_id&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rel_type&lt;/span&gt;  &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Indexing JSON in Oracle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'R. Houlihan'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Vector search at scale'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="s1"&gt;'A. Lovelace'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Graph queries with SQL/PGQ'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'G. Hopper'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Functional indexes on JSON_VALUE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.10, 0.20, 0.30, 0.40]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Approximate nearest neighbor with HNSW'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.11, 0.21, 0.31, 0.42]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;knowledge_base&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Property graph traversal in 23ai+'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.50, 0.60, 0.70, 0.80]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Indexes'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HNSW'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'SQL/PGQ'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;kg_edges&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'related_to'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;kg_edges&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'used_by'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;PROPERTY&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt; &lt;span class="n"&gt;knowledge_graph&lt;/span&gt;
  &lt;span class="n"&gt;VERTEX&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;EDGE&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kg_edges&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;edge_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;SOURCE&lt;/span&gt;      &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;DESTINATION&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;kg_nodes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="n"&gt;PROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;WITH&lt;/span&gt;
&lt;span class="n"&gt;semantic_matches&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- Vector similarity search (literal probe vector — bind one in production)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.10, 0.20, 0.30, 0.40]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                         &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;knowledge_base&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
  &lt;span class="k"&gt;FETCH&lt;/span&gt;  &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;graph_context&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- Graph traversal (SQL/PGQ). GRAPH_TABLE projects the doc_id alongside&lt;/span&gt;
  &lt;span class="c1"&gt;-- the matched entities so the join can stay outside the operator.&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;related&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rel_type&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;GRAPH_TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;knowledge_graph&lt;/span&gt;
           &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="n"&gt;COLUMNS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;source_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;related&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rel_type&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;semantic_matches&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_doc&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;enriched&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- Relational metadata&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;semantic_matches&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt;   &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'results'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;     &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'score'&lt;/span&gt;     &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'relations'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'entity'&lt;/span&gt;  &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'related'&lt;/span&gt; &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;related&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'type'&lt;/span&gt;    &lt;span class="n"&gt;VALUE&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rel_type&lt;/span&gt;
                        &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;graph_context&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;
      &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;enriched&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rag_context&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vector search. Graph traversal. Relational joins. JSON construction. One query. One transaction. One optimizer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Alternative: Polyglot Persistence
&lt;/h3&gt;

&lt;p&gt;Let's walk through building the same feature on a "purpose-built" stack. You know the pitch — "use the right tool for the job." Here are the jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector similarity search&lt;/strong&gt; → vector database (Pinecone, Weaviate, or Atlas Search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graph traversal&lt;/strong&gt; → graph database (Neo4j, Neptune)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document storage&lt;/strong&gt; → MongoDB or DynamoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relational metadata&lt;/strong&gt; → Postgres or MySQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assembly&lt;/strong&gt; → your application code, on an EC2 instance somewhere, holding the bag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now your request path looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;HTTP call out to the vector service. Marshall the request, send it over TCP, wait for the response, unmarshall 20 results.&lt;/li&gt;
&lt;li&gt;HTTP call out to the graph database. Pass the IDs from step 1. Marshall, send, wait, unmarshall.&lt;/li&gt;
&lt;li&gt;HTTP call out to the document store. Pass IDs again. Marshall, send, wait, unmarshall.&lt;/li&gt;
&lt;li&gt;HTTP call out to the relational database. Pass IDs again. Marshall, send, wait, unmarshall.&lt;/li&gt;
&lt;li&gt;Write application code to JOIN all four result sets in-memory.&lt;/li&gt;
&lt;li&gt;Hand-build the JSON response object.&lt;/li&gt;
&lt;li&gt;Ship it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Count the overhead. Four network round-trips. Four serialization cycles. Four deserialization cycles. Four connection pools. Four auth handshakes. Four monitoring dashboards. Four SDKs with four different query APIs your team has to learn. And the developer — not the database — is the one writing the join logic that used to be &lt;code&gt;FROM a JOIN b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;"But we have a unified API!" — okay, let's actually look at that. A unified API over multiple backend services reduces the &lt;em&gt;number of network calls your app makes&lt;/em&gt;, because the vendor's gateway is now doing the fan-out instead of your code. That's a real win for developer ergonomics. It is also the entire win.&lt;/p&gt;

&lt;p&gt;What the unified API does &lt;strong&gt;not&lt;/strong&gt; give you is a unified data model. MongoDB, for instance, exposes document storage, Atlas Search, vector search, time series, and a bolt-on graph primitive (&lt;code&gt;$graphLookup&lt;/code&gt;) through a common surface. Looks clean. But underneath, there's still no relational model, no cost-based optimizer that spans those stores, and no way to join four data shapes in a single execution plan. You can &lt;code&gt;$lookup&lt;/code&gt; your way to something that resembles a join, but you're stitching stages in an aggregation pipeline — the developer is still the optimizer, and cross-shard &lt;code&gt;$graphLookup&lt;/code&gt; can't even participate in a multi-document transaction. Consistency? Atlas Search indexes update in a separate process with one-to-fifteen seconds of lag. Your vector results and your document results can — and routinely do — disagree about what's actually in the database right now.&lt;/p&gt;

&lt;p&gt;So yes, the unified API saves you some network round trips. It does not save you from the thing that breaks your RAG pipeline at 3am: the underlying stores don't share a model, don't share an optimizer, and don't share a transaction boundary. The seams are still there. You just can't see them anymore — and neither can your debugger.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Consistency Problem
&lt;/h3&gt;

&lt;p&gt;Now the part that isn't obvious until it's production.&lt;/p&gt;

&lt;p&gt;In the polyglot model, every service has its own clock. The vector database indexed your document three seconds ago. The graph database processed the relationship change yesterday. The document store has today's data. The relational metadata is fresh from the last ETL job that ran at 4am. All four sources are &lt;em&gt;individually correct&lt;/em&gt; and &lt;em&gt;collectively lying&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When you're building a dashboard, you shrug. When you're feeding a retrieval-augmented LLM, you don't. The model gets vector results pointing to a document that no longer exists, graph relationships that were severed an hour ago, and metadata from a snapshot taken last night. What does the model do with contradictory facts?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It hallucinates. With confidence.&lt;/strong&gt; And your users — or worse, your agents taking actions on behalf of your users — treat that hallucination as truth.&lt;/p&gt;

&lt;p&gt;There's no application fix for this. You can't retry your way out of it. You can't throw more monitoring at it. The inconsistency is baked into the architecture the moment you decided to split your data across systems that don't share a transaction boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Converged Engine Gives You
&lt;/h3&gt;

&lt;p&gt;In the Oracle query above, the CBO sees the entire statement. It knows the vector search returns ~20 rows, so it'll probably use that as the driving input. It knows the graph traversal is bounded by a predicate. It knows the &lt;code&gt;documents&lt;/code&gt; join can use an index. It builds &lt;strong&gt;one execution plan&lt;/strong&gt; that's optimized across vector, graph, relational, and JSON operations simultaneously.&lt;/p&gt;

&lt;p&gt;But the part that matters for your users is this: &lt;strong&gt;every piece of data in the response came from the same moment in time.&lt;/strong&gt; The vector search, the graph traversal, and the relational join all ran inside the same transaction against the same snapshot. There's no "the vector index says this but the document says that." There's just one answer, internally consistent, from one source of truth.&lt;/p&gt;

&lt;p&gt;For an LLM, that's the difference between grounding and gambling. For an autonomous agent, it's the difference between correct action and expensive liability. For you — the developer on the hook when the 3am page comes in — it's the difference between "the model got confused" and "the system did what it was supposed to do."&lt;/p&gt;

&lt;p&gt;One query. One transaction. One optimizer. One truth. That's not a feature. That's architecture — and no amount of clever SDK design on top of a polyglot stack gets you there.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Indexing and Performance
&lt;/h2&gt;

&lt;p&gt;Good SQL/JSON queries are fast. Properly indexed SQL/JSON queries are blazing. Wrong indexes — or missing ones — turn beautiful declarative SQL into a sequential scan of your entire table, which is an experience every developer enjoys exactly once before they learn to run &lt;code&gt;EXPLAIN PLAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Oracle gives you five different kinds of JSON indexes, each optimized for a different access pattern. Here's the decision tree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you're doing&lt;/th&gt;
&lt;th&gt;Use this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Querying one scalar field, known queries&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Functional index&lt;/strong&gt; on &lt;code&gt;JSON_VALUE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same field queried with multiple types&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Multiple functional indexes&lt;/strong&gt; — one per type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looking up values inside a JSON array&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Multivalue index&lt;/strong&gt; (21c+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad-hoc or unknown query patterns across the whole document&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;JSON search index&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only want to index rows matching a condition&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;WHERE&lt;/code&gt; to any of the above — &lt;strong&gt;partial index&lt;/strong&gt; (23ai+)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CBO picks whichever one matches your predicate. You don't hint, you don't route — you just write SQL and Oracle figures out which index to use. Most production workloads combine two or three of these. Let's walk through them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Functional Indexes on JSON_VALUE
&lt;/h3&gt;

&lt;p&gt;For known access patterns — the queries your app runs a million times a day:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.status'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Composite index across multiple JSON fields&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_cust_date&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.customer.name'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.orderDate'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use dot-notation with item methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_customer&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical rule: the expression has to match exactly.&lt;/strong&gt; The CBO compares the index definition and the query predicate as &lt;em&gt;expressions&lt;/em&gt;, not as semantic equivalents. "Close enough" is not enough. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Return type must match.&lt;/strong&gt; &lt;code&gt;RETURNING NUMBER&lt;/code&gt; in the index and a query without a &lt;code&gt;RETURNING&lt;/code&gt; clause (which defaults to &lt;code&gt;VARCHAR2&lt;/code&gt;) = no match. Index ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VARCHAR2 length is part of the type.&lt;/strong&gt; &lt;code&gt;RETURNING VARCHAR2(100)&lt;/code&gt; in the index and &lt;code&gt;RETURNING VARCHAR2(400)&lt;/code&gt; in the query = no match. These are different type instances as far as the optimizer is concerned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dot-notation and JSON_VALUE are different expressions.&lt;/strong&gt; An index created with &lt;code&gt;o.order_doc.customer.string()&lt;/code&gt; will &lt;strong&gt;not&lt;/strong&gt; be used by a query written with &lt;code&gt;JSON_VALUE(order_doc, '$.customer' RETURNING VARCHAR2(100))&lt;/code&gt; — even though both compute "the customer field as a string." The item method &lt;code&gt;.string()&lt;/code&gt; is internally equivalent to &lt;code&gt;JSON_VALUE&lt;/code&gt; with a default VARCHAR2 return (typically VARCHAR2(4000)), and that's rarely the same type as your &lt;code&gt;RETURNING VARCHAR2(n)&lt;/code&gt; index definition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The practical rule:&lt;/strong&gt; pick &lt;strong&gt;one syntax&lt;/strong&gt; for each access path — either dot-notation with item methods, or &lt;code&gt;JSON_VALUE&lt;/code&gt; with explicit &lt;code&gt;RETURNING&lt;/code&gt; clauses — and use it consistently for both the index definition and every query that should hit it. Mixing them is the single most common reason "my index isn't getting used" in Oracle SQL/JSON.&lt;/p&gt;

&lt;p&gt;If you find out too late that your queries and your index are using different expressions, you have two options: rebuild the index to match, or change the queries to match the index. There's no magic reconciliation. Run &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; or check &lt;code&gt;DBMS_XPLAN.DISPLAY_CURSOR&lt;/code&gt; — if you see a full table scan where you expected an index range scan, expression mismatch is almost always the culprit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if your queries hit the same path with different types?&lt;/strong&gt; Create multiple functional indexes — one per type. Each one is a thin B-tree, each one matches exactly one query shape, and the CBO picks whichever one fits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Index for string comparisons: WHERE amount.string() = '100.00'&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_amount_str&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;-- Index for numeric comparisons: WHERE amount.number() &amp;gt; 100&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_amount_num&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You pay for the extra storage and write amplification on that field, but every query shape gets index access. We will discuss how to avoid this in a minute. This is the "explicit coverage" approach — and it's especially useful for legacy code paths that query the same field with different conventions than your newer code.&lt;/p&gt;

&lt;p&gt;If you'd rather not enumerate types by hand, the next option covers every type at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Search Indexes: Index Everything, Query Anywhere
&lt;/h3&gt;

&lt;p&gt;Functional and multivalue indexes are targeted — they cover specific paths you already know you'll query. But document data is document data: schemas drift, new fields appear, analysts show up with questions nobody anticipated, multi-tenant customers each care about different attributes. What do you do when you don't know the access patterns in advance?&lt;/p&gt;

&lt;p&gt;You create a &lt;strong&gt;JSON search index&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one statement makes every path in every document indexed. &lt;code&gt;$.customer&lt;/code&gt;? Indexed. &lt;code&gt;$.shipping.address.city&lt;/code&gt;? Indexed. &lt;code&gt;$.items[*]?(@.category == "electronics")&lt;/code&gt;? Indexed. &lt;code&gt;$.totally.new.field.you.added.last.tuesday&lt;/code&gt;? Indexed, automatically, the moment data with that path shows up. You didn't declare any of those paths individually. You didn't add DDL when the schema evolved. The index adapts.&lt;/p&gt;

&lt;p&gt;Any &lt;code&gt;JSON_EXISTS&lt;/code&gt;, &lt;code&gt;JSON_VALUE&lt;/code&gt;, or full-text predicate on that column becomes a candidate for index access. The CBO chooses it automatically when no more targeted index fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparisons with MongoDB — the honest version.&lt;/strong&gt; People draw a few different comparisons between Oracle's JSON search index and MongoDB's indexing options. Some are closer than others. The accurate mapping:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MongoDB&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;What they both do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wildcard index &lt;code&gt;{ "$**": 1 }&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;JSON search index (scalar use)&lt;/td&gt;
&lt;td&gt;Index every path for ad-hoc queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlas Search&lt;/td&gt;
&lt;td&gt;JSON search index (full-text use)&lt;/td&gt;
&lt;td&gt;Above + tokenization, stemming, phrase, regex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multikey index&lt;/td&gt;
&lt;td&gt;Multivalue index&lt;/td&gt;
&lt;td&gt;Per-field array indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compound index&lt;/td&gt;
&lt;td&gt;Composite functional index&lt;/td&gt;
&lt;td&gt;Multiple fields in one index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;wildcard index&lt;/strong&gt; is the closest comparison when you're just talking about "index everything for unknown access patterns" — both let you skip the up-front decision of which fields to index, and both stay in the operational engine (no sidecar). That's the fair comparison and it's worth giving MongoDB credit for the feature.&lt;/p&gt;

&lt;p&gt;Where wildcard indexes fall short for production use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One field per query.&lt;/strong&gt; A wildcard index can only help a query that predicates on a single field. Multi-field queries like &lt;code&gt;{ customer: "Acme", status: "shipped" }&lt;/code&gt; can't use the wildcard index for both predicates — it picks one and scans for the other. Compound queries still need compound indexes, which means you're back to enumerating access patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No complex filters.&lt;/strong&gt; Oracle's &lt;code&gt;JSON_EXISTS&lt;/code&gt; with filter predicates (&lt;code&gt;?(@.price &amp;gt; 100 &amp;amp;&amp;amp; @.category in ("electronics","tools"))&lt;/code&gt;) gets evaluated at the index level. MongoDB's wildcard index can't support that class of predicate — it has to scan after the index narrows things down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can't be unique, can't be TTL, can't be a shard key.&lt;/strong&gt; A lot of functionality you give up for the convenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No full-text.&lt;/strong&gt; If you want tokenization, stemming, or phrase search, you stand up &lt;strong&gt;Atlas Search&lt;/strong&gt; as a separate process. Separate process means separate sync cycle, and here's the punchline: &lt;strong&gt;Atlas Search runs as a sidecar&lt;/strong&gt; (&lt;code&gt;mongot&lt;/code&gt;) with one to fifteen seconds of indexing lag. During that window, your document exists but isn't searchable. Your RAG pipeline reads the vector result and then can't find the corresponding document because the search index is playing catch-up. Your support team files a ticket that says "the search is broken." Again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oracle's JSON search index does all of it — scalar ad-hoc queries, complex filter predicates, full-text search — from a single index definition, &lt;strong&gt;in-kernel and transactionally consistent&lt;/strong&gt; by default. The index updates inside the same transaction that updates the row. Commit returns. The document is searchable. Period. No sidecar process. No replication lag. No "eventual" anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Transactional (default)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
  &lt;span class="k"&gt;PARAMETERS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SYNC (ON COMMIT)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Or periodic, if you'd rather trade freshness for write throughput&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SEARCH&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_search&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;
  &lt;span class="k"&gt;PARAMETERS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SYNC (EVERY "FREQ=SECONDLY; INTERVAL=1")'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your choice, your trade-off. Oracle's default is consistency. MongoDB's only option is async.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use it:&lt;/strong&gt; Analytics workloads. Multi-tenant systems where each tenant queries different fields. Applications where the schema evolves faster than you can deploy new DDL. Anywhere you'd otherwise be tempted to stand up Elasticsearch as a sidecar just to make ad-hoc queries fast. The JSON search index gives you that capability inside your operational database, consistent with your operational data, without standing up new infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off:&lt;/strong&gt; it's heavier to maintain than a targeted functional index. Every insert tokenizes the whole document and updates the inverted lists. For write-heavy OLTP workloads with well-known access patterns, targeted functional indexes are still the better call. Most real systems use both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two or three functional indexes on the hot-path fields (status, customer, date, whatever drives 80% of your queries)&lt;/li&gt;
&lt;li&gt;One JSON search index as the catch-all for everything else&lt;/li&gt;
&lt;li&gt;A couple of partial indexes layered on top for high-cardinality optional fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CBO picks whichever index fits the query. You write normal SQL and let the optimizer sort it out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multivalue Indexes (21c+)
&lt;/h3&gt;

&lt;p&gt;For indexing values inside JSON arrays — something ordinary functional indexes can't do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MULTIVALUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- This query uses the multivalue index&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;JSON_EXISTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.tags[*]?(@.string() == "wholesale")'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Execution plans show &lt;code&gt;INDEX RANGE SCAN (MULTI VALUE)&lt;/code&gt; when the optimizer picks it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on type coverage for array values.&lt;/strong&gt; The multivalue index above is locked to &lt;code&gt;.string()&lt;/code&gt; — it indexes the tag values as text. If your arrays can hold values of different types (say, a &lt;code&gt;metrics&lt;/code&gt; array that mixes numbers and strings, or an audit log with mixed scalar types), the same pattern applies as with scalar indexes: create one multivalue index per type you actually query on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Tags queried as strings&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MULTIVALUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;-- Numeric IDs queried as numbers&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MULTIVALUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_ids_num&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relatedIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;MongoDB's multikey indexes are strict about BSON types&lt;/strong&gt; — and this is where developers get burned. Say two services write to the same &lt;code&gt;tags&lt;/code&gt; array, one as strings and one as numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inserted by Service A&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;urgent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;    &lt;span class="c1"&gt;// "123" is a string&lt;/span&gt;

&lt;span class="c1"&gt;// Inserted by Service B&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;urgent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// 123 is a number&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;          &lt;span class="c1"&gt;// returns _id: 2 only&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;        &lt;span class="c1"&gt;// returns _id: 1 only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither query finds both documents. MongoDB's BSON comparison treats &lt;code&gt;"123"&lt;/code&gt; and &lt;code&gt;123&lt;/code&gt; as different values, so the index stores them as different entries and queries silently miss across the type boundary. To catch both, you need &lt;code&gt;$in: [123, "123"]&lt;/code&gt; — and now your application code has to know the full set of type variations that might exist in the database, which defeats the point of having a schema-flexible store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oracle's multivalue indexes normalize at index time.&lt;/strong&gt; When you create a multivalue index with &lt;code&gt;.string()&lt;/code&gt;, both &lt;code&gt;"123"&lt;/code&gt; and &lt;code&gt;123&lt;/code&gt; are stored as the string &lt;code&gt;"123"&lt;/code&gt; in the index. When you create one with &lt;code&gt;.number()&lt;/code&gt;, both are stored as the number &lt;code&gt;123&lt;/code&gt;. You pick the canonical representation and the index enforces it, regardless of how heterogeneous the source data is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Both "123" and 123 are indexed as the string "123"&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MULTIVALUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;-- Both "123" and 123 are indexed as the number 123&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MULTIVALUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_order_tags_num&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query using &lt;code&gt;@.string() == "123"&lt;/code&gt; on the string index hits both documents. A query using &lt;code&gt;@.number() == 123&lt;/code&gt; on the number index hits both documents. You choose the type view once at index creation time and get consistent results forever, without your app code having to enumerate every type variation the source data might contain.&lt;/p&gt;

&lt;p&gt;If you want broad coverage across multiple types without declaring each one by hand, use a JSON search index — it catches everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sparse Indexes: Skip Rows You Don't Care About
&lt;/h3&gt;

&lt;p&gt;Here's a question every developer asks once they start putting indexes on optional JSON fields: &lt;em&gt;if most of my documents don't have a &lt;code&gt;trackingUrl&lt;/code&gt;, am I still paying to index NULL on every insert?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For single-column B-tree indexes, the answer is automatic — Oracle doesn't index rows where the indexed column is NULL. An index on &lt;code&gt;JSON_VALUE(order_doc, '$.trackingUrl')&lt;/code&gt; skips unshipped orders for free.&lt;/p&gt;

&lt;p&gt;For everything else — composite indexes, indexes you want to skip based on conditions other than NULL — wrap the index expression in a &lt;code&gt;CASE&lt;/code&gt; that returns &lt;code&gt;NULL&lt;/code&gt; for the rows you want to exclude. Oracle treats those rows as un-indexed, the same way it treats genuine NULLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Only index trackingUrl on shipped orders. The CASE returns NULL for&lt;/span&gt;
&lt;span class="c1"&gt;-- non-shipped rows, and Oracle skips NULLs from the B-tree.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_shipped_tracking&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.status'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'shipped'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.trackingUrl'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rows where status isn't &lt;code&gt;'shipped'&lt;/code&gt; are never stored in this B-tree — not on insert, not on update, not on delete. The index stays small and the write path stays fast. The CBO matches a query of the form &lt;code&gt;WHERE CASE WHEN ... = 'shipped' THEN ... END = '&amp;lt;url&amp;gt;'&lt;/code&gt; straight to this index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern: Type-Routed Indexes for Polymorphic Documents
&lt;/h3&gt;

&lt;p&gt;Now combine the &lt;code&gt;CASE&lt;/code&gt; trick with the multi-type coverage pattern and you get something MongoDB can't replicate cleanly: &lt;strong&gt;write-path routing based on document shape.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of real-world document collections are polymorphic. One table holds customers, products, and invoices. Or one table holds v1 documents with &lt;code&gt;amount&lt;/code&gt; stored as a string (&lt;code&gt;"1,234.56"&lt;/code&gt;) and v2 documents with &lt;code&gt;amount&lt;/code&gt; stored as a &lt;code&gt;NUMBER&lt;/code&gt;. Standard indexing says you either pick one type and break the other, or you index the lowest-common-denominator representation and lose query precision. Neither is great.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CASE&lt;/code&gt;-gated indexes let you define one index per shape and route writes to the right one based on a discriminator attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Schema-version routing: each row contributes to exactly one of these&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_amount_v1&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.schemaVersion'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.amount'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_amount_v2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.schemaVersion'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.amount'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you insert a v1 document, only &lt;code&gt;idx_amount_v1&lt;/code&gt; gets maintained. When you insert a v2 document, only &lt;code&gt;idx_amount_v2&lt;/code&gt;. Neither index stores NULL entries for the other version, and neither contends with the other on the write path. The total write amplification stays the same as a single-version index even though the collection handles multiple schemas simultaneously.&lt;/p&gt;

&lt;p&gt;The same pattern works for polymorphic entity types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- One table, multiple entity types, write-path-isolated indexes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_customer_name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.entityType'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.companyName'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_product_sku&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.entityType'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'product'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.sku'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_invoice_number&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.entityType'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'invoice'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;JSON_VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.invoiceNo'&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Insert a customer document: only the customer index gets touched. Insert a product: only the product index. Insert an invoice: only the invoice index. Write overhead becomes &lt;strong&gt;proportional to the entity type you're writing&lt;/strong&gt;, not to the total number of indexes on the table.&lt;/p&gt;

&lt;p&gt;This is the cleanest answer to the "polymorphic collection" problem I've seen in any database. You don't pay for indexes that don't apply. You don't deal with NULL pollution. You don't need to split the data across multiple tables to get write-path isolation. And query time, the CBO picks the right index automatically based on the predicate — no hints, no routing logic in application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trade-off:&lt;/strong&gt; schema-version or entity-type discriminators have to be discoverable from the document. Make sure your write path sets them consistently — these become load-bearing fields. But if you're already tagging documents with a type or version (and most polyglot migrations do), you get write-path routing for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  The OSON Advantage
&lt;/h3&gt;

&lt;p&gt;Every one of these indexes is riding on top of the native JSON type's OSON binary format — and that matters more than most developers realize.&lt;/p&gt;

&lt;p&gt;Here's why. When your JSON is stored as text (VARCHAR2, CLOB, or MongoDB's BSON), every field access has to walk the document byte-by-byte until it finds the right offset. O(n) complexity per lookup, where n is how deep into the document the field lives. Every index maintenance operation — every insert, every update — pays that parse cost again. At scale, this shows up as CPU burn on your write path and latency spikes on deeply nested field queries.&lt;/p&gt;

&lt;p&gt;OSON doesn't parse. It &lt;strong&gt;navigates.&lt;/strong&gt; Field names are hash-indexed, so looking up &lt;code&gt;$.shipping.address.zip&lt;/code&gt; is three hash jumps — direct offset lookups, not scans. O(1) per field, regardless of document size or nesting depth. Your index maintenance is cheaper because the underlying field access is cheaper. Your queries are cheaper. Your writes are cheaper.&lt;/p&gt;

&lt;p&gt;I've benchmarked this extensively. At field position 1000 in a document, OSON is &lt;strong&gt;529x faster&lt;/strong&gt; than BSON for field access. At position 50, it's &lt;strong&gt;28.6x&lt;/strong&gt;. This isn't marketing. It's storage engine mechanics — hash-indexed O(1) versus length-prefixed sequential O(n). Same input, same output, radically different physics.&lt;/p&gt;

&lt;p&gt;And because OSON supports &lt;strong&gt;piecewise updates&lt;/strong&gt;, modifying one field in a large document only writes the changed portion to disk, undo, and redo. Update &lt;code&gt;$.status&lt;/code&gt; on a 2MB document and you pay for 2KB of I/O, not 2MB. Functional indexes on unchanged fields don't get touched at all. Text-stored JSON has to rewrite the entire column on every update — including BSON, which has no piecewise update path at the storage layer.&lt;/p&gt;

&lt;p&gt;For the deep dive on the binary format mechanics, see my article: &lt;em&gt;Why Binary Document Protocols Aren't All Created Equal&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bottom line for developers:&lt;/strong&gt; the index features in this section work on top of OSON because OSON makes them practical. You can have type-routed partial indexes and full-document search indexes and multivalue arrays and targeted functional indexes on the same column because the underlying format is fast enough that maintaining all of them on every write is affordable. On a text-based or sequentially-scanned format, this mix of indexing strategies would murder your write throughput. On OSON, it's just how you build things.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;We've covered the mechanics. Let's zoom out.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Schema Validation (26ai)
&lt;/h3&gt;

&lt;p&gt;Oracle 26ai lets you enforce document structure at the storage layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;validated_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="s1"&gt;'{
    "type": "object",
    "properties": {
      "orderId":  {"type": "number"},
      "customer": {"type": "string"},
      "items":    {"type": "array", "items": {
        "type": "object",
        "required": ["product", "quantity"]
      } }
    },
    "required": ["orderId", "customer", "items"]
  }'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid documents are rejected at insert time with &lt;code&gt;ORA-40875&lt;/code&gt;. No application-layer validation. No "trust the client" hope. The database enforces the contract. Oracle is the first major relational database to support JSON Schema validation natively as a constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON_SERIALIZE: Making Binary Readable
&lt;/h3&gt;

&lt;p&gt;When you need to inspect OSON binary content as text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Pretty print with alphabetically sorted keys (26ai). The clauses&lt;/span&gt;
&lt;span class="c1"&gt;-- must come in the order: RETURNING &amp;lt;type&amp;gt;, PRETTY, ORDERED.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_SERIALIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_doc&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PRETTY&lt;/span&gt; &lt;span class="n"&gt;ORDERED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essential for debugging, logging, and API responses. The &lt;code&gt;ORDERED&lt;/code&gt; keyword (26ai) sorts keys alphabetically — useful for deterministic output in tests and diffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Relational Duality Views: The UMT Implementation
&lt;/h3&gt;

&lt;p&gt;Everything in this article leads here. Duality Views are the physical implementation of Unified Model Theory — one truth (normalized relational tables), many shapes (JSON document views), zero data duplication.&lt;/p&gt;

&lt;p&gt;Throughout this article we've worked with JSON-storage tables and projected them into relational shapes via &lt;code&gt;JSON_TABLE&lt;/code&gt;. Duality Views go the &lt;strong&gt;other&lt;/strong&gt; direction: normalized relational tables are the source of truth, and the view projects them as updatable JSON documents. To make the example concrete, here's the relational schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dv_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt;      &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_name&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dv_order_items&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;item_id&lt;/span&gt;      &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;order_id&lt;/span&gt;     &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;dv_orders&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_name&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;quantity&lt;/span&gt;     &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;unit_price&lt;/span&gt;   &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dv_orders&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Acme Corp'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dv_orders&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Beta LLC'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dv_order_items&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Widget Pro'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dv_order_items&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cable Kit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dv_order_items&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Gadget Plus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;RELATIONAL&lt;/span&gt; &lt;span class="n"&gt;DUALITY&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;order_dv&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'_id'&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;-- root PK must be projected&lt;/span&gt;
    &lt;span class="s1"&gt;'customer'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'items'&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                     &lt;span class="s1"&gt;'_id'&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- nested PK must also be projected&lt;/span&gt;
                     &lt;span class="s1"&gt;'product'&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s1"&gt;'quantity'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s1"&gt;'price'&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;
                   &lt;span class="p"&gt;}&lt;/span&gt;
                   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dv_order_items&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
                   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dv_orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a fully updatable JSON document view over normalized relational tables. INSERT a document through the view and it decomposes into relational rows across multiple tables. Query through the view and it assembles documents from those tables. Full ACID. Optimistic concurrency via ETags. Accessible via SQL, REST, or even the MongoDB wire protocol.&lt;/p&gt;

&lt;p&gt;Model the domain. Project the access. One truth. Many shapes. Zero tradeoffs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Oracle's SQL/JSON isn't a compatibility checkbox. It's not "we also do JSON." It's a complete query language for hierarchical data, integrated into the most mature optimizer in the industry, backed by a binary format designed by database research veterans, and extended with construction, transformation, validation, and duality features that don't exist anywhere else.&lt;/p&gt;

&lt;p&gt;But the real story for developers isn't the feature list. It's what these features eliminate from your stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code That Stops Existing
&lt;/h3&gt;

&lt;p&gt;Look at the shape of a typical application built around a traditional database. You have an ORM translating between objects and tables, because your tables don't look like your objects. You have DTO classes shuttling data between the ORM layer and your API layer, because your ORM objects don't look like your API responses. You have serialization code building JSON from those DTOs, because your API consumers want JSON. You have validation libraries on top of all of it, because none of those layers enforce a contract.&lt;/p&gt;

&lt;p&gt;And when your JSON data doesn't fit the relational model cleanly, you stand up a second database — a document store, a search engine, a vector index — and now you have integration code. Change data capture. Sync jobs. Consistency compensators. Retry logic. Pipelines that fail at 2am. Tickets nobody wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oracle SQL/JSON collapses this stack.&lt;/strong&gt; Your tables can hold both normalized relational data and JSON documents. Your queries return JSON directly, shaped to match your API contract. Your indexes cover both data shapes. Your transactions span both models. Your optimizer plans across both.&lt;/p&gt;

&lt;p&gt;Count the code that stops existing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ORM required.&lt;/strong&gt; Your query emits the JSON structure your API consumer wants. &lt;code&gt;res.send(row.api_response)&lt;/code&gt; is the whole handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No DTO layer.&lt;/strong&gt; There's nothing to translate. The database returned the final shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No serialization library.&lt;/strong&gt; The database did the serialization, and it did it on the binary OSON structure, not in your application memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation framework for the API response.&lt;/strong&gt; JSON Schema validation runs at the storage layer, not the application layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No integration layer between document and relational data.&lt;/strong&gt; Both live in the same table, same transaction, same optimizer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sidecar search or vector services.&lt;/strong&gt; JSON search and native VECTOR types live inside the database, consistent with the operational data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No consistency compensators.&lt;/strong&gt; The engine gives you ACID across JSON, relational, graph, and vector operations in the same transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is a class of code that exists in a polyglot architecture because the underlying databases don't talk to each other. Eliminate the need for them to talk to each other, and the code evaporates. What's left is the part of your application that actually creates business value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let Physics Define Your Data Model
&lt;/h3&gt;

&lt;p&gt;Here's the principle that should drive every storage decision you make: &lt;strong&gt;if data is accessed together, store it together. If access patterns aren't clear, normalize it.&lt;/strong&gt; That's it. That's the rule. The database's job is to make both possible without forcing you to choose an architecture up front.&lt;/p&gt;

&lt;p&gt;All data is ultimately relational — entities, attributes, and the relationships between them. That's not a philosophy; that's how information works. But storing everything as normalized relational tables is an implementation choice, and it's the wrong choice when your access patterns are well-known and involve reading hierarchical data together. You're paying for joins you don't need, on data you always fetch as a unit, to get back a result you have to re-hierarchize in application code anyway.&lt;/p&gt;

&lt;p&gt;The physics argument goes the other way too. When your access patterns are unknown, varied, or analytical — when different consumers need the same data in different shapes, or when you need aggregation across entities — normalized relational is the only sensible choice. Embedding everything in documents means duplicating data, fighting update anomalies, and losing the ability to reason efficiently about relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Oracle lets you pick per-workload without committing to a paradigm.&lt;/strong&gt; You have the full toolkit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native JSON type columns.&lt;/strong&gt; Store documents as first-class data alongside relational columns in the same table. When your access patterns are well-known and you want document locality, put the document in the row. One read, one fetch, one shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Collections (SODA / MongoDB API).&lt;/strong&gt; Pure document storage with the MongoDB wire protocol on top. Drop-in compatibility for existing document applications, with all the indexing, ACID, and optimizer capabilities of the Oracle engine underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid tables.&lt;/strong&gt; Relational columns for structured, queryable fields alongside a JSON column for flexible, schema-optional attributes. The best of both worlds when your data has both known structure and emergent properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalized relational tables.&lt;/strong&gt; When access patterns are varied, analytical workloads matter, and entity relationships drive the queries, normal form is still the right answer. Use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Relational Duality Views.&lt;/strong&gt; When the same canonical data needs to serve multiple access patterns with multiple document shapes, model once in relational form and project as many document views as you need. Updates flow both ways with full ACID guarantees. One source of truth, many projections, no CDC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these stores the same kind of data. Every one participates in the same transactions. Every one is indexed and optimized by the same cost-based optimizer. You don't have to pick one approach for the whole application — different tables can use different strategies based on how the data is actually accessed. &lt;strong&gt;Let the physics of your access patterns define the model, not the other way around.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the answer to "should we use a document database or a relational database?" that the industry has been arguing about for fifteen years. The answer isn't one or the other. The answer is: &lt;strong&gt;whichever shape fits the access pattern, with full SQL and full JSON support either way, inside a single engine with a single transaction boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application code sees documents where documents make sense. Your analytics queries see tables where tables make sense. Your reports see normalized joins. Your AI agents see vector-indexed context. Nobody sees the seams because there are no seams.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your New Stack
&lt;/h3&gt;

&lt;p&gt;The developer who actually internalizes what Oracle SQL/JSON can do doesn't just write faster JSON queries. They build fundamentally simpler systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No ORM.&lt;/strong&gt; The database returns your API response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sidecar services.&lt;/strong&gt; Search, vector, graph, and relational share the same engine and transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sync jobs.&lt;/strong&gt; There's nothing to sync — one truth, many projections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No complex middleware.&lt;/strong&gt; The query is the middleware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No "which database does this belong in?" debates.&lt;/strong&gt; The answer is always "this one."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No praying that five systems agree on current state.&lt;/strong&gt; They're all the same system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write one query. You get one truth. Your application is smaller, your operational surface is smaller, your bug reports are fewer, and your pages at 3am have better reasons than "the search index is lagging again."&lt;/p&gt;

&lt;p&gt;That's not a feature. That's physics. ⚡&lt;/p&gt;




</description>
      <category>database</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
