Object Tables   «Prev  Next»

Lesson 7Practical SQL Examples
ObjectiveWrite ordinary SQL queries against Oracle 26ai object tables and inspect their execution in PL/SQL.

Query Oracle 26ai Object Tables: Practical SQL Examples

An Oracle object table stores rows whose declared structure comes from a SQL object type. You can select its scalar attributes with familiar SQL: name the attributes in SELECT, identify the table in FROM, and restrict the rows with WHERE. You do not need a special object-navigation operator simply to retrieve a name or identifier.

This lesson develops that first querying technique in Oracle AI Database 26ai. It uses a small animal dataset so every query has defined prerequisites and a predictable purpose. The same pattern applies to a customer object table whose type declares cust_id, first_name and last_name. Those attribute names must actually exist in the type; SQL does not infer them from the table's name.

Keep the result shape in mind. Selecting three scalar attributes returns three ordinary result columns. Selecting an entire object is a different projection, introduced briefly at the end. A relational table can also contain an object-valued column, which adds a column name to the attribute path. Recognizing these two storage arrangements prevents many confusing dot-notation errors.

SELECT a.animal_id, a.name, a.breed
FROM animal a
WHERE a.breed = 'ALLIGATOR'
ORDER BY a.animal_id;
The parts of an object-table query
ClauseRole in this query
SELECTProjects three attributes as separate result columns.
FROMIdentifies the object table and gives it the alias a.
WHERERetains rows that satisfy the breed predicate.
ORDER BYMakes the result order explicit.

Read this as a complete query, not as a sequence of procedural instructions to the database. The optimizer chooses an execution plan that preserves SQL semantics. The aliases and predicates describe the result you want, rather than promising a particular access path or the order in which Oracle evaluates expressions.

Prepare a consistent practice dataset

Run the following definitions in a practice schema where these names are unused. You need privileges to create types and tables, plus appropriate tablespace quota. These examples and their expected results were checked against Oracle documentation, but were not executed against an Oracle instance. Do not run CREATE OR REPLACE indiscriminately against shared types with dependent tables.

CREATE TYPE animal_location_ty AS OBJECT (
  city       VARCHAR2(40 CHAR),
  enclosure  VARCHAR2(20 CHAR)
);
/
CREATE TYPE animal_ty AS OBJECT (
  animal_id  NUMBER,
  name       VARCHAR2(30 CHAR),
  breed      VARCHAR2(30 CHAR),
  location   animal_location_ty
);
/
CREATE TABLE animal OF animal_ty (
  animal_id PRIMARY KEY
);

INSERT INTO animal VALUES (
  animal_ty(1, 'FRANCES', 'CAT',
            animal_location_ty('ORLANDO', 'A1'))
);
INSERT INTO animal VALUES (
  animal_ty(2, 'BENJI', 'DOG',
            animal_location_ty('ORLANDO', 'B1'))
);
INSERT INTO animal VALUES (
  animal_ty(3, 'LYLE', 'ALLIGATOR',
            animal_location_ty('MIAMI', 'C1'))
);
COMMIT;

The location type is created first because animal_ty uses it as an attribute datatype. The table then stores row objects of animal_ty. Each constructor supplies values in the attribute order declared by its type. An object type defines structure; its definition alone neither stores these three animals nor creates the table.

The slash submits a type definition in script-oriented clients such as SQL*Plus. It is a client command, not part of the CREATE TYPE SQL sent through a database driver. Follow your client's execution mode when running the script. The INSERT statements use SQL constructors and the final COMMIT makes the inserted practice data permanent in this transaction.

The primary key belongs to the table definition and makes animal_id unique and non-null there. Other attribute declarations do not establish additional business rules automatically. For example, the type's breed attribute permits a null value unless the table has an applicable constraint. Decide which integrity rules belong in a real application rather than inferring them from descriptive attribute names.

Select top-level attributes directly

SELECT a.animal_id, a.name, a.breed
FROM animal a
ORDER BY a.animal_id;

The expected rows, in identifier order, are:

ANIMAL_ID  NAME     BREED
---------  -------  ---------
1          FRANCES  CAT
2          BENJI    DOG
3          LYLE     ALLIGATOR

The headings and spacing shown here are illustrative. Your client may format NUMBER and VARCHAR2 values differently. The important result is three rows with the selected attributes. The object type name animal_ty is not inserted between the table alias and a top-level attribute: a.name is the appropriate path.

An unqualified SELECT name FROM animal is possible for this simple projection. Aliases make the ownership clearer and become important when a query grows to include joins or nested attributes. Use them consistently so that the naming pattern remains understandable when the query is maintained later.

A projection is also an interface decision. SELECT * can be useful during exploration, but explicitly naming the attributes documents which values the caller expects. If a type evolves or an application reads results by position, an unchecked wildcard can expose an unintended interface change. Choose a projection deliberately rather than assuming object tables require whole-object retrieval.

Filter the rows and control the order

SELECT a.name
FROM animal a
WHERE a.breed = 'ALLIGATOR'
ORDER BY a.name, a.animal_id;
Why this query returns LYLE
ElementMeaning
a.nameThe result contains the name attribute, not the complete animal object.
animal aThe source is the animal object table, referenced through alias a.
Breed predicateOnly the row whose breed equals ALLIGATOR qualifies in the supplied data.
OrderingThe identifier provides a tie-breaker if names repeat.

The expected name is LYLE. A string literal belongs in single quotes. For an application parameter, bind the supplied breed value instead of building SQL by concatenating user input. A parameterized version can use WHERE a.breed = :wanted_breed when the execution environment supplies that bind variable.

Bind syntax is not a complete standalone script: the client or application must define and supply the bind. The literal query above remains the self-contained teaching example. Separating these two forms avoids presenting an unexplained placeholder as if it were a value already stored in the database.

WHERE and SELECT answer different questions. You can filter on breed while returning only name. You can likewise order by animal_id without including it in this simple SELECT list. For grouped or distinct queries, inspect the applicable SQL restrictions rather than transferring every rule from a basic projection without checking.

Without ORDER BY, the apparent insertion order of these rows is not a reporting guarantee. A different execution plan or additional data can change the order you observe. For stable pagination or reproducible exercises, choose a complete ordering with a unique tie-breaker. This is a SQL result requirement, not a special property of objects.

Navigate an embedded object attribute

SELECT a.name, a.location.city AS city,
       a.location.enclosure AS enclosure
FROM animal a
WHERE a.location.city = 'ORLANDO'
ORDER BY a.animal_id;

Here location is an attribute of the animal row object, and city is an attribute of the embedded location object. The path a.location.city starts with the table alias and follows the declared structure. The expected matches are FRANCES in enclosure A1 and BENJI in enclosure B1.

Oracle requires a table alias for dot-notational references to object attributes. Naming the table without an alias is not an interchangeable substitute in those expressions. The alias also reduces the risk of confusing an object attribute with another column introduced by a later query change.

You need to navigate location only when the query uses its contents. Selecting a.name does not require selecting or spelling out every embedded attribute. The old claim that nested object attributes must be referenced during every SELECT, UPDATE and DELETE is too broad. The requested projection and predicate determine which attribute paths are necessary.

Consider missing values explicitly. A predicate such as a.location.city = 'ORLANDO' does not match a null city because the comparison is unknown. IS NULL tests for a missing scalar city. Also distinguish a null location object from an existing location object whose individual attributes are null; they represent different states even when a selected scalar value appears empty.

Contrast row objects with an object-valued column

CREATE TABLE animal_registry (
  registry_id  NUMBER PRIMARY KEY,
  animal_info  animal_ty,
  recorded_on  DATE
);

INSERT INTO animal_registry
SELECT a.animal_id, VALUE(a), DATE '2026-10-09'
FROM animal a;
COMMIT;

SELECT r.registry_id, r.animal_info.name AS animal_name,
       r.animal_info.location.city AS city
FROM animal_registry r
ORDER BY r.registry_id;

Animal_registry is an ordinary relational table. Its animal_info column holds an animal_ty value, alongside registry_id and recorded_on. The name path therefore includes the column: r.animal_info.name. In the animal object table, the corresponding path was a.name because each row itself was the object.

VALUE(a) copies the row object's value into the destination object-valued column in this INSERT SELECT. It does not establish a live link that synchronizes the two tables. A later update to animal does not automatically revise animal_registry. In a real design, define whether a registry contains a snapshot, an independent value or a relationship to an entity.

The date literal is independent of language-specific date display settings. The output still depends on the client's formatting, but the inserted value is unambiguous. This small example defines every referenced table and type, rather than relying on an unexplained sample schema being present.

Group scalar attributes for a report

SELECT a.location.city AS city, COUNT(*) AS animal_count
FROM animal a
GROUP BY a.location.city
ORDER BY city;

The supplied data illustrates one animal in MIAMI and two in ORLANDO. GROUP BY combines rows with the same selected city expression; COUNT(*) counts the rows in each group. This query compares scalar city values and does not require comparing complete animal objects.

If null city values exist, they form a group of their own. COUNT(a.location.city) would omit null city expressions within a group, so it is not interchangeable with COUNT(*) for counting registered animals. Name the aggregate to communicate what it measures, and include a null case when checking a report against realistic data.

Retrieve one row in PL/SQL

A client SELECT returns a result set. A scalar SELECT INTO in PL/SQL instead assigns one row to variables. Its contract matters: zero rows raises NO_DATA_FOUND, and multiple rows raises TOO_MANY_ROWS. Handle those outcomes according to the application's requirement rather than treating them as ordinary empty result sets.

SET SERVEROUTPUT ON
DECLARE
  l_name animal.name%TYPE;
BEGIN
  SELECT a.name INTO l_name
  FROM animal a
  WHERE a.animal_id = 3;

  DBMS_OUTPUT.PUT_LINE('Animal: ' || l_name);
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    DBMS_OUTPUT.PUT_LINE('No animal has that identifier.');
  WHEN TOO_MANY_ROWS THEN
    DBMS_OUTPUT.PUT_LINE('The lookup returned multiple animals.');
END;
/

The practice data illustrates Animal: LYLE. Changing the identifier to 999 exercises the missing-row branch. The primary key prevents multiple matches for this particular identifier lookup; the second handler shows the separate cardinality outcome if a future query uses a nonunique criterion.

There is no need to test SQL%FOUND after this successful scalar lookup. If it returned normally, one row was assigned. If it found no row, execution already transferred to the exception handler. Adding IF SQL%NOTFOUND immediately after the SELECT does not stop that exception. The handler should reflect a business outcome; unexpected database errors should not be disguised as a missing animal.

Process several rows with a cursor FOR loop

BEGIN
  FOR r IN (
    SELECT a.animal_id, a.name
    FROM animal a
    ORDER BY a.animal_id
  ) LOOP
    DBMS_OUTPUT.PUT_LINE(r.animal_id || ': ' || r.name);
  END LOOP;
END;
/

The loop illustrates three output lines in identifier order. It manages the query cursor and supplies a record for each iteration. An empty result executes the body zero times. Use this shape when several rows are expected, rather than weakening a scalar lookup until its multiple-row exception disappears.

For a task expressible as one SQL statement, a procedural loop is not automatically the best implementation. This example teaches row processing and output, not a performance recommendation. Do not use SQL%ROWCOUNT to count the loop's query rows; count iterations explicitly if the application needs that total.

Read implicit cursor status after object-table DML

Status of the most recent implicit operation in PL/SQL
AttributeMeaning
SQL%ROWCOUNTThe affected-row count after successful DML; it is not a general count of a client's query results.
SQL%FOUNDTrue when the implicit operation affects or returns a row; false for a zero-row DML operation.
SQL%NOTFOUNDThe corresponding no-row status; it does not replace scalar SELECT INTO exception handling.
SQL%ISOPENAlways false for the implicit SQL cursor.

Before any relevant implicit operation, FOUND, NOTFOUND and ROWCOUNT are null. Subsequent SQL, including SQL executed inside a called routine, can replace the status being observed. Capture what you need immediately after the target statement. Explicit named cursors have their own cursor_name%attribute interface.

DECLARE
  l_rows PLS_INTEGER;
BEGIN
  UPDATE animal a
  SET a.breed = 'CROCODILIAN'
  WHERE a.animal_id = 3;
  l_rows := SQL%ROWCOUNT;

  IF l_rows = 0 THEN
    DBMS_OUTPUT.PUT_LINE('No matching animal.');
  ELSE
    DBMS_OUTPUT.PUT_LINE('Rows updated: ' || l_rows);
  END IF;
END;
/
ROLLBACK;

This optional continuation illustrates one updated row. The rollback restores the committed ALLIGATOR value, provided the exercise runs in the same session without intervening commits. Rows affected are not evidence that a transaction committed. An update can count a matching row even if the assigned value equals its existing value.

After a multiple-row scalar SELECT INTO error, SQL%ROWCOUNT is not the number of matching rows. Do not use it to diagnose the dataset's cardinality. Choose a separate query or a multiple-row processing interface when that count is part of the requirement.

Recognize the boundary of this technique

SELECT VALUE(a) AS animal_object
FROM animal a
WHERE a.animal_id = 3;

This final comparison projects one typed object value instead of several scalar attributes. Its client representation may differ from an ordinary row of text and numbers. VALUE takes the object-table alias, while the earlier projections name individual attributes. The next lesson develops querying more complex objects.

For everyday reports, start by identifying the desired result columns, the qualifying rows and the required order. Then check whether the source stores row objects or object-valued columns, and write the correct aliased paths. In PL/SQL, decide whether the consumer expects exactly one row or several. These choices make the query's behavior clear before cursor status or procedural logic is added.

Oracle 26ai documentation: object attribute name resolution, object storage, VALUE, implicit cursor attributes, and cursor behavior.

SEMrush Software 7 SEMrush Banner 7