<?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>DEV Community: Rick Houlihan</title>
    <description>The latest articles on DEV Community by Rick Houlihan (@rick_houlihan_cf110dba340).</description>
    <link>https://dev.to/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>DEV Community: Rick Houlihan</title>
      <link>https://dev.to/rick_houlihan_cf110dba340</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rick_houlihan_cf110dba340"/>
    <language>en</language>
    <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://dev.to/rick_houlihan_cf110dba340/oracle-sqljson-the-developers-guide-to-querying-json-like-a-pro-3hmf</link>
      <guid>https://dev.to/rick_houlihan_cf110dba340/oracle-sqljson-the-developers-guide-to-querying-json-like-a-pro-3hmf</guid>
      <description>&lt;h1&gt;
  
  
  Oracle SQL/JSON: The Developer's Guide to Querying JSON Like a Pro
&lt;/h1&gt;

&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;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:&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;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="n"&gt;order_doc&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;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;'{
  "orderId": 1001,
  "customer": "Acme Corp",
  "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;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&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;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;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&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="k"&gt;last&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;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'&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;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;-- ERROR ON ERROR: missing path raises ORA exception&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="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="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;-- DEFAULT ON ERROR: missing path returns your fallback&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="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="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;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;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 it at the session level:&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;/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. Toggle it off when you're done debugging, or set it to &lt;code&gt;NULL&lt;/code&gt; if you'd rather have permissive defaults.&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'&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_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'&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&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;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&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;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;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;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="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;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 'addresses' is an array of objects&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;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'$.addresses?(@.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="n"&gt;JSON_TABLE&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;'$'&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;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="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.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;metadata&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;'$.metadata'&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_notes&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;'$.notes'&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;/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'&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="n"&gt;NESTED&lt;/span&gt; &lt;span class="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.sections[*]'&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;section_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="n"&gt;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.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;'$.paragraphs[*]'&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;para_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;PATH&lt;/span&gt; &lt;span class="s1"&gt;'$.text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;para_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;/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="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="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="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="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="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;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="p"&gt;...&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="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="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step2&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;ON&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;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=" "&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:&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;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;/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="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;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'&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'&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;
  
  
  26ai Enhancements: NESTED PATH and CASE
&lt;/h3&gt;

&lt;p&gt;Apply transformations to every element in an array:&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;-- Apply 10% discount to every item&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="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="k"&gt;SET&lt;/span&gt; &lt;span class="s1"&gt;'@.discountedPrice'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'@.unitPrice * 0.9'&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;Conditional transformations:&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;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&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;'"shipped"'&lt;/span&gt;
      &lt;span class="k"&gt;THEN&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;true&lt;/span&gt;
    &lt;span class="k"&gt;ELSE&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;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="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'&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;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;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&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="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&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)&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&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;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="k"&gt;WHERE&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;IN&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;doc_id&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="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;name&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="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="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;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'&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_tags_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;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_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_tags_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;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_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;
  
  
  Partial Indexes: Exclude NULL, Route Writes
&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 every 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 exclude based on conditions other than NULL — &lt;strong&gt;partial indexes&lt;/strong&gt; (23ai+) let you attach a &lt;code&gt;WHERE&lt;/code&gt; clause directly to the index definition:&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_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="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="p"&gt;)&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;'$.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="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 &lt;strong&gt;never touched&lt;/strong&gt; by this index — not on insert, not on update, not on delete. The B-tree stays small and the write path stays fast.&lt;/p&gt;

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

&lt;p&gt;Now combine partial indexes 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 NUMBER, because you evolved the schema without running a big-bang migration. 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;Partial indexes let you define one index per shape and route writes to the right index 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&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="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="p"&gt;)&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;'$.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="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="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="p"&gt;)&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;'$.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="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. Neither index 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="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="p"&gt;)&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;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="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="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="p"&gt;)&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;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="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="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="p"&gt;)&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;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="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)&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;ORDERED&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="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;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;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;'orderId'&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="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;'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;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;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;




&lt;p&gt;&lt;em&gt;What SQL/JSON patterns are you using in production? Have you tried Duality Views yet? I'd love to hear what's working — and what's not.&lt;/em&gt; 👇&lt;/p&gt;

&lt;h1&gt;
  
  
  Oracle #JSON #SQL #DatabaseArchitecture #SQLJson #DeveloperTools #DataModeling #OracleDatabase #CTE #UnifiedModelTheory #DataEngineering #Performance #EnterpriseAI #TechLeadership
&lt;/h1&gt;

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