Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

PL/SQL JSON Functions in Oracle Databases

Oracle Database has robust support for handling JSON data directly within SQL and PL/SQL. This guide explores the key built-in functions that empower developers to seamlessly integrate JSON processing into their database applications, from storing and creating JSON to querying, modifying, and indexing it efficiently.

Why Use JSON in Oracle Database?

JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate. Its flexible, schema-less nature makes it ideal for many modern applications, especially those dealing with diverse or evolving data structures.

Integrating JSON support directly into the database engine allows you to:

  • Store schema-flexible data alongside traditional relational data.
  • Process JSON data without extracting it into application code.
  • Leverage Oracle's strengths like ACID transactions, security, and scalability for JSON data.
  • Use familiar SQL syntax to interact with JSON.

Storing JSON Data

Oracle offers several ways to store JSON data in columns:

  • VARCHAR2, CLOB, BLOB: You can store JSON text as strings in these traditional datatypes. However, Oracle recommends using the native JSON datatype for better performance and validation.
  • JSON Datatype (Oracle 12c Release 2 and later): This native datatype stores JSON in a binary format optimized for queries and updates. It also ensures that the data stored is valid JSON. Oracle internally manages the storage format (often OSON - Oracle Spatial and Graph JSON binary format).

Example: Using the JSON Datatype

CREATE TABLE products (
    product_id NUMBER PRIMARY KEY,
    product_info JSON
);

INSERT INTO products (product_id, product_info)
VALUES (101, '{"name": "Laptop", "brand": "TechCo", "specs": {"cpu": "i7", "ram": 16}}');

INSERT INTO products (product_id, product_info)
VALUES (102, '{"name": "Keyboard", "brand": "Accessory inc", "color": "Black"}');

-- Inserting invalid JSON will raise an error with the JSON datatype
-- INSERT INTO products (product_id, product_info)
-- VALUES (103, '{"name": "Mouse", "brand": "Logi"');

When using VARCHAR2 or CLOB, you can add an IS JSON check constraint to ensure data validity, although the native JSON type is generally preferred.

Creating JSON with SQL/PL/SQL

Oracle provides functions to construct JSON objects and arrays from SQL query results or PL/SQL variables.

  • JSON_OBJECT: Creates a JSON object from name-value pairs.
  • JSON_ARRAY: Creates a JSON array from a list of values.
  • JSON_ARRAYAGG: Aggregates values from multiple rows into a single JSON array.
  • JSON_OBJECTAGG: Aggregates key-value pairs from multiple rows into a single JSON object.

Example: Creating JSON with Functions

-- Creating a simple JSON object
SELECT JSON_OBJECT('name' VALUE 'Alice', 'age' VALUE 30) AS simple_json_object FROM dual;
-- Output: {"name":"Alice","age":30}

-- Creating a JSON object with a nested object
SELECT JSON_OBJECT('person' VALUE JSON_OBJECT('name' VALUE 'Bob', 'city' VALUE 'London')) AS nested_json_object FROM dual;
-- Output: {"person":{"name":"Bob","city":"London"}}

-- Creating a simple JSON array
SELECT JSON_ARRAY(1, 2, 3, 'apple', TRUE) AS simple_json_array FROM dual;
-- Output: [1,2,3,"apple",true]

-- Creating a JSON array of objects from table data (example table: employees)
-- Assume employees table has columns employee_id, name, job
/*
CREATE TABLE employees (
    employee_id NUMBER PRIMARY KEY,
    name VARCHAR2(100),
    job VARCHAR2(100)
);
INSERT INTO employees VALUES (1, 'Alice', 'Engineer');
INSERT INTO employees VALUES (2, 'Bob', 'Manager');
INSERT INTO employees VALUES (3, 'Charlie', 'Analyst');
*/
SELECT JSON_ARRAYAGG(
           JSON_OBJECT(
               'id'   VALUE employee_id,
               'name' VALUE name,
               'role' VALUE job
           ) ORDER BY employee_id
       ) AS employees_json_array FROM employees;
/*
Output (example):
[
  {"id":1,"name":"Alice","role":"Engineer"},
  {"id":2,"name":"Bob","role":"Manager"},
  {"id":3,"name":"Charlie","role":"Analyst"}
]
*/

-- Creating a JSON object where keys are employee IDs and values are employee names
SELECT JSON_OBJECTAGG(employee_id VALUE name) AS employees_json_object FROM employees;
/*
Output (example):
{"1":"Alice","2":"Bob","3":"Charlie"}
*/

The ABSENT ON NULL and NULL ON NULL clauses can be used with JSON_OBJECT andJSON_ARRAY to control whether null values result in the key/value being absent or present with a JSON null. By default, it's usually NULL ON NULL.

Querying JSON Data

Extracting data from JSON stored in Oracle is done using powerful functions that leverage SQL/JSON path expressions, similar to XPath for XML.

  • JSON_VALUE: Extracts a scalar value (string, number, boolean, null) from JSON. Returns NULL by default if the path is not found or the result is not scalar.
  • JSON_QUERY: Extracts a JSON object or array from JSON. Returns NULL by default if the path is not found or the result is scalar.
  • JSON_EXISTS: Checks if a specific path exists within the JSON data. Returns TRUE or FALSE.
  • JSON_TABLE: Transforms JSON data into relational rows and columns, allowing you to query JSON structures as if they were tables. This is arguably the most powerful function for complex queries.

Example: Querying JSON with Functions

-- Using the products table created earlier
-- Select product name using JSON_VALUE
SELECT product_id, JSON_VALUE(product_info, '$.name') AS product_name FROM products WHERE product_id = 101;
-- Output: 101, Laptop

-- Select the 'specs' object using JSON_QUERY
SELECT product_id, JSON_QUERY(product_info, '$.specs') AS product_specs FROM products WHERE product_id = 101;
-- Output: 101, {"cpu":"i7","ram":16}

-- Check if a product has a 'color' property using JSON_EXISTS
SELECT product_id, JSON_EXISTS(product_info, '$.color') AS has_color FROM products;
/*
Output:
101, FALSE
102, TRUE
*/

-- Extract a nested scalar value using JSON_VALUE
SELECT product_id, JSON_VALUE(product_info, '$.specs.cpu') AS cpu FROM products WHERE product_id = 101;
-- Output: 101, i7

-- Handle missing paths or invalid types with ON ERROR and ON EMPTY clauses (JSON_VALUE defaults: NULL ON ERROR, NULL ON EMPTY)
SELECT product_id,
       JSON_VALUE(product_info, '$.price' DEFAULT 'N/A' ON EMPTY) AS product_price, -- Path does not exist
       JSON_VALUE(product_info, '$.specs' ERROR ON ERROR) AS product_specs_value -- specs is an object, not scalar
FROM products WHERE product_id = 101;
-- Output: 101, N/A (for price), Error (for specs)

-- Using JSON_TABLE to extract multiple columns from JSON
SELECT p.product_id, jt.name, jt.brand, jt.cpu, jt.ram
FROM products p,
     JSON_TABLE(p.product_info, '$' -- Path to the root object
         COLUMNS (
             name  VARCHAR2(100) PATH '$.name',
             brand VARCHAR2(100) PATH '$.brand',
             -- Nested path for specs
             NESTED PATH '$.specs' -- Path to the nested object/array
                 COLUMNS (
                     cpu VARCHAR2(50) PATH '$.cpu',
                     ram NUMBER       PATH '$.ram'
                 )
         )
     ) jt;
/*
Output:
PRODUCT_ID, NAME    , BRAND          , CPU, RAM
----------------------------------------------------
101       , Laptop  , TechCo         , i7 , 16
102       , Keyboard, Accessory inc  ,    ,
*/

-- Using JSON_TABLE to flatten an array within JSON (example structure: {"order_id": 1, "items": [{"item_id": 10, "qty": 2}, {"item_id": 20, "qty": 1}]})
/*
CREATE TABLE orders (
    order_id NUMBER PRIMARY KEY,
    order_details JSON
);
INSERT INTO orders VALUES (1, '{"order_id": 1, "items": [{"item_id": 10, "qty": 2}, {"item_id": 20, "qty": 1}]}');
INSERT INTO orders VALUES (2, '{"order_id": 2, "items": [{"item_id": 30, "qty": 5}]}');
*/
SELECT o.order_id, item.item_id, item.qty
FROM orders o,
     JSON_TABLE(o.order_details, '$.items[*]' -- Path to the array and '*' iterates elements
         COLUMNS (
             item_id NUMBER PATH '$.item_id',
             qty     NUMBER PATH '$.qty'
         )
     ) item;
/*
Output:
ORDER_ID, ITEM_ID, QTY
-----------------------
1       , 10     , 2
1       , 20     , 1
2       , 30     , 5
*/

SQL/JSON path expressions start with $, representing the context item (usually the JSON document itself). Use .key for object members and [index] for array elements (0-based). [*] iterates over all elements in an array.

Modifying JSON Data

Oracle provides the JSON_TRANSFORM function (Oracle 18c and later) to perform various mutation operations on JSON data efficiently.

Example: Modifying JSON with JSON_TRANSFORM

-- Using the products table
-- Add a new field 'price'
SELECT JSON_TRANSFORM(product_info, SET '$.price' = 599.99) AS updated_json FROM products WHERE product_id = 101;
-- Output: {"name":"Laptop","brand":"TechCo","specs":{"cpu":"i7","ram":16},"price":599.99}

-- Update an existing field 'brand'
SELECT JSON_TRANSFORM(product_info, SET '$.brand' = 'Acme Corp') AS updated_json FROM products WHERE product_id = 102;
-- Output: {"name":"Keyboard","brand":"Acme Corp","color":"Black"}

-- Remove a field 'color'
SELECT JSON_TRANSFORM(product_info, REMOVE '$.color') AS updated_json FROM products WHERE product_id = 102;
-- Output: {"name":"Keyboard","brand":"Accessory inc"} -- Note: The example is SELECT, UPDATE would modify the table

-- Insert an element into an array (if specs.accessories is an array)
-- Let's assume product_info for 101 is '{"name":"Laptop","brand":"TechCo","specs":{"cpu":"i7","ram":16,"accessories":[]}}'
-- SELECT JSON_TRANSFORM(product_info, INSERT '$.specs.accessories[0]' = 'Mouse') AS updated_json FROM products WHERE product_id = 101;
-- Output: {"name":"Laptop","brand":"TechCo","specs":{"cpu":"i7","ram":16,"accessories":["Mouse"]}}

-- Multiple operations in one call
SELECT JSON_TRANSFORM(product_info,
                      SET    '$.price' = 649.50,
                      REMOVE '$.color'
       ) AS updated_json FROM products WHERE product_id = 102;
-- Output: {"name":"Keyboard","brand":"Accessory inc","price":649.5} -- Price added, color removed

-- Example UPDATE statement using JSON_TRANSFORM
-- UPDATE products
-- SET product_info = JSON_TRANSFORM(product_info, SET '$.price' = 649.50)
-- WHERE product_id = 101;

JSON_TRANSFORM supports operations like SET, INSERT, REPLACE, ADD, and REMOVE.ADD is used for adding elements to arrays, while INSERT can be used to insert at a specific index.

Indexing JSON Data

Effective indexing is crucial for query performance, especially with large JSON datasets. Oracle offers several options:

  • JSON Search Index (Oracle 12c Release 2 and later): A special type of index optimized for searching within JSON documents. It can index scalar values, arrays, or even index everything. It's a domain index (like TEXT indexes). Oracle 21c introduced a faster type of JSON search index.
  • Function-Based Index: Create a standard B-tree index on the result of a JSON function (like JSON_VALUE) for a specific, frequently queried scalar path.
  • B-tree Index on Virtual Columns: Create a virtual column that extracts a scalar value using JSON_VALUE, then index the virtual column. Similar to a function-based index but the extracted value is represented as a column.

Example: Creating Indexes on JSON

-- Using the products table

-- Create a JSON Search Index (indexes scalar values by default)
CREATE SEARCH INDEX products_json_search_idx ON products (product_info) FOR JSON;

-- Create a Function-Based Index on a specific path (e.g., product name)
CREATE INDEX products_name_fbi ON products (JSON_VALUE(product_info, '$.name'));

-- Create a Virtual Column and index it (alternative to FBI)
ALTER TABLE products ADD (product_name_vc VARCHAR2(100) GENERATED ALWAYS AS (JSON_VALUE(product_info, '$.name')));
CREATE INDEX products_name_vc_idx ON products (product_name_vc);

-- JSON Search Index for indexing specific paths or types (More advanced)
-- CREATE SEARCH INDEX products_json_complex_idx ON products (product_info) FOR JSON
-- PARAMETERS ('INCLUDE PATHS ($.name, $.brand, $.specs.cpu) DATAGUIDE ON');

Choose the indexing strategy based on your query patterns. Function-based indexes/virtual columns are great for queries filtering on specific scalar values. JSON Search indexes are more versatile for searches involving multiple paths or predicates within the JSON.

Querying JSON without appropriate indexes can lead to full table scans and poor performance, especially on large tables. Always analyze your query execution plans.

Performance and Best Practices

To ensure optimal performance when working with JSON in Oracle:

  • Use the native JSON datatype if possible (12c R2+).
  • Design appropriate indexes based on your most frequent query patterns.
  • Use JSON_VALUE for extracting single scalar values and JSON_QUERY for objects/arrays.
  • Leverage JSON_TABLE for complex queries that need to join or filter on multiple JSON fields or flatten arrays.
  • Be mindful of the ON ERROR and ON EMPTY clauses in query functions, as their default behavior might impact performance or results.
  • Avoid storing massive JSON documents in a single column if you frequently need to query small parts of them; consider normalizing structures or using appropriate indexing.
  • Keep your Oracle Database version updated to benefit from performance enhancements in JSON processing.

Conclusion

Oracle Database provides a comprehensive set of functions for handling JSON data, enabling developers to build applications that leverage the flexibility of JSON while benefiting from the robustness and performance of the Oracle platform. By understanding and utilizing functions like JSON_OBJECT, JSON_ARRAYAGG, JSON_VALUE, JSON_QUERY, JSON_TABLE, JSON_TRANSFORM, and the various indexing options, you can effectively work with JSON data directly within your SQL and PL/SQL code.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool