Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

PL/SQL & T-SQL (Oracle and SQL Server)  Module 14 – Performance: plans, PL/SQL called from SQL and table functions

Pipelined table functions and SQL macros

Stream rows from PL/SQL to a query with pipelined table functions, clean up with NO_DATA_NEEDED when it stops early, and build queries with table SQL macros.

  • Advanced
  • 15 minutes
  • Examples run with oracle-free 23.26.3.0.0, mssql 17.0.5005.3, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: A pipelined function hands each row over with PIPE ROW · What the function sees when its query stops early · Pipelined function, table SQL macro or polymorphic table function. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Write a pipelined table function that hands rows to a query with PIPE ROW
  • Handle NO_DATA_NEEDED so a function cleans up when its query stops reading early
  • Choose between a pipelined function, a table SQL macro and a polymorphic table function
  • Predict the query a table SQL macro puts in place of its call
  • Explain what a parallel-enabled pipelined function needs from its input

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a pipelined table function that hands rows to a query with PIPE ROW
  • Handle NO_DATA_NEEDED so a function cleans up when its query stops reading early
  • Choose between a pipelined function, a table SQL macro and a polymorphic table function
  • Predict the query a table SQL macro puts in place of its call
  • Explain what a parallel-enabled pipelined function needs from its input

Before you start

On this page

A report that shows the first 6 months of a 240-month loan schedule needs 6 rows, yet a table function that returns a collection builds all 240 and holds them in memory before the query sees month 1. pipelined table function: A PL/SQL table function declared PIPELINED that hands each row to the query with PIPE ROW as soon as it is built, instead of building a whole collection and returning it at the end. hand each row to the query as soon as it is built, so early rows arrive early and the work stops once the query has enough.

You will write a pipelined schedule, let it clean up when a query stops early, and choose between a pipelined function, a table SQL macro and a polymorphic table function.

A pipelined function hands each row over with PIPE ROW

The practice function emi_schedule builds a loan’s repayment plan: per month, the part that repays the loan, the interest and the balance left. Each payment is the same Equated monthly instalment: the same payment every month, worked out so that it repays a loan and its interest over a fixed number of months.: 17,254.84 a month for 100,000 at 12 % a year over 6 months. Step through the rows as they leave:

Interactive · message by message One schedule row at a time, from PIPE ROW to the query Step through the six-payment schedule as the function builds each row and hands it to the query.
One schedule row at a time, from PIPE ROW to the query, step 5 of 5. Messages so far: The query to Pipelined function: rows for 6 months; Pipelined function to The query: month 1 row; Pipelined function to The query: month 2 row; Pipelined function to The query: month 6 row.The queryPipelined functionrows for 6 monthsmonth 1 rowmonth 2 rowmonth 6 row

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The query opens the function. The query asks emi_schedule for the rows of a loan of 100,000 at 12 % a year over 6 months. The function works out the monthly payment once: 17,254.84.
  2. Month 1 is built and piped. Interest for month 1 is 1 % of 100,000, so 1,000.00; the rest of the payment, 16,254.84, repays the loan and leaves 83,745.16. PIPE ROW hands this row to the query and the loop goes on.
  3. Month 2 follows the same way. Interest is now 1 % of the smaller balance, 837.45, so more of the payment repays the loan: 16,417.39. The function never holds more than the row it is building.
  4. The last month clears the loan. Month 6 repays whatever is left, 17,083.98, with 170.84 of interest, so the balance ends at 0.00 instead of a rounding remainder. Its payment is 17,254.82, 0.02 less than the others.
  5. RETURN ends the function. After the last PIPE ROW the function runs RETURN with no value: every row has already gone to the query. The query has six rows and a total interest of 3,529.02.

Pipelined table functions say so in the header, RETURN emi_rows_t PIPELINED, with a collection type declared at schema level or in a package, SQL element types, never an associative array. PIPE ROW sends one element to the query without giving up control, and the final RETURN carries no value. A query reads the function like a table:

emi_schedule.sql: a pipelined loan schedule, its totals and a 6-row read of 240 months SQL · pipeline/emi_schedule.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- A pipelined table function that builds a loan's monthly schedule one row at a time and hands each row to the
-- query with PIPE ROW. Amounts are rounded to 2 places; the last month repays whatever balance is left.
SET FEEDBACK OFF

-- Drop what an earlier run left, the function first because it depends on the types.
DROP FUNCTION IF EXISTS emi_schedule;
DROP TYPE IF EXISTS emi_rows_t;
DROP TYPE IF EXISTS emi_row_t;

-- The row and the collection the function returns: SQL types, so a query can read them.
CREATE TYPE emi_row_t AS OBJECT (
  month_num      NUMBER(4),
  principal_part NUMBER(14, 2),
  interest       NUMBER(14, 2),
  balance        NUMBER(14, 2)
);
/

CREATE TYPE emi_rows_t AS TABLE OF emi_row_t;
/

CREATE FUNCTION emi_schedule (
  p_principal   IN NUMBER,   -- the amount borrowed
  p_annual_rate IN NUMBER,   -- yearly interest rate, in per cent
  p_months      IN NUMBER    -- number of monthly payments
) RETURN emi_rows_t PIPELINED
IS
  v_rate    NUMBER := p_annual_rate / 100 / 12;
  v_payment NUMBER;
  v_balance NUMBER := p_principal;
  v_int     NUMBER;
  v_part    NUMBER;
BEGIN
  -- Equal monthly payment: P * r * (1 + r)^n / ((1 + r)^n - 1), rounded to 2 places.
  v_payment := ROUND(p_principal * v_rate * POWER(1 + v_rate, p_months)
                     / (POWER(1 + v_rate, p_months) - 1), 2);
  FOR m IN 1 .. p_months LOOP
    v_int := ROUND(v_balance * v_rate, 2);
    IF m = p_months THEN
      v_part := v_balance;              -- the last payment absorbs the rounding
    ELSE
      v_part := v_payment - v_int;
    END IF;
    v_balance := v_balance - v_part;
    PIPE ROW (emi_row_t(m, v_part, v_int, v_balance));
  END LOOP;
  RETURN;                               -- nothing to return: every row was piped
END emi_schedule;
/

SET FEEDBACK ON
-- 1. Six payments on 100,000 at 12 % a year. Expected: six rows, month 1 = 16254.84, 1000.00, 83745.16;
--    month 6 = 17083.98, 170.84, 0.00 (examples/model/stream_model.py computes the same rows).
SELECT month_num, principal_part, interest, balance
FROM   emi_schedule(100000, 12, 6);

-- 2. Totals over the same rows. Expected: total_interest 3529.02, total_paid 103529.02.
SELECT SUM(interest) AS total_interest, SUM(principal_part + interest) AS total_paid
FROM   emi_schedule(100000, 12, 6);

-- 3. A 240-month loan, read only as far as the report needs. Expected: six rows, months 1 to 6.
SELECT month_num, principal_part, interest, balance
FROM   emi_schedule(100000, 12, 240)
FETCH FIRST 6 ROWS ONLY;

-- Leave the schema as it was.
DROP FUNCTION emi_schedule;
DROP TYPE emi_rows_t;
DROP TYPE emi_row_t;
emi_schedule.sql, run in the lab Python · emi_schedule_session.py
"""emi_schedule.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("pipeline/emi_schedule.sql")

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @pipeline/emi_schedule.sql
 MONTH_NUM PRINCIPAL_PART   INTEREST    BALANCE
---------- -------------- ---------- ----------
         1       16254.84       1000   83745.16
         2       16417.39     837.45   67327.77
         3       16581.56     673.28   50746.21
         4       16747.38     507.46   33998.83
         5       16914.85     339.99   17083.98
         6       17083.98     170.84          0

6 rows selected.


TOTAL_INTEREST TOTAL_PAID
-------------- ----------
       3529.02  103529.02

1 row selected.


 MONTH_NUM PRINCIPAL_PART   INTEREST    BALANCE
---------- -------------- ---------- ----------
         1         101.09       1000   99898.91
         2          102.1     998.99   99796.81
         3         103.12     997.97   99693.69
         4         104.15     996.94   99589.54
         5         105.19      995.9   99484.35
         6         106.25     994.84    99378.1

6 rows selected.


Function dropped.


Type dropped.


Type dropped.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 emi_schedule_session.py

Its expected rows come from this model, with the same rounding: a generator’s yield plays PIPE ROW, and a list plays a function that returns the whole collection. Change the loan and run it:

stream_model.py: the six rows, and how many rows each style builds for a 6-row read Python · model/stream_model.py
"""A model of the lesson's pipelined EMI schedule, in Python's own terms.

A Python generator hands back one value per yield, the way a pipelined table
function hands back one row per PIPE ROW. Closing a generator early raises
GeneratorExit at the paused yield, the way PIPE ROW raises NO_DATA_NEEDED when
the query needs no more rows. Amounts are rounded half up to 2 places, as
Oracle's ROUND rounds these positive amounts. The model has no batches: the
PL/SQL runtime hands piped rows over in batches, so a real function can build
a few rows more than the query reads before it is stopped.
"""

from decimal import ROUND_HALF_UP, Decimal
from itertools import islice

CENT = Decimal("0.01")


def monthly_payment(principal, annual_rate, months):
    """The equated monthly instalment, as emi_schedule.sql computes it."""
    r = Decimal(annual_rate) / 100 / 12
    f = (1 + r) ** months
    return (Decimal(principal) * r * f / (f - 1)).quantize(CENT, ROUND_HALF_UP)


def schedule(principal, annual_rate, months, built):
    """Yield (month, principal part, interest, balance) one row at a time."""
    r = Decimal(annual_rate) / 100 / 12
    pay = monthly_payment(principal, annual_rate, months)
    balance = Decimal(principal)
    try:
        for month in range(1, months + 1):
            interest = (balance * r).quantize(CENT, ROUND_HALF_UP)
            # The last month repays what is left, so the balance ends at 0.00.
            part = balance if month == months else pay - interest
            balance -= part
            built.append(month)
            yield month, part, interest, balance
    except GeneratorExit:
        built.append("stopped")  # the NO_DATA_NEEDED moment: clean up here
        raise


built = []
rows = list(schedule(100000, 12, 6, built))
print("Loan 100000 at 12% a year over 6 months, payment",
      monthly_payment(100000, 12, 6))
print("month  principal  interest    balance")
for month, part, interest, balance in rows:
    print(f"{month:>5}  {part:>9}  {interest:>8}  {balance:>9}")
total_interest = sum(row[2] for row in rows)
print("total interest", total_interest, "| last payment", rows[-1][1] + rows[-1][2])

# A collection-returning function builds every row before the caller sees one.
built_list = []
whole = list(schedule(100000, 12, 240, built_list))
first_six = whole[:6]
print("collection: rows built before month 1 reached the caller:", len(built_list))

# A pipelined function hands rows over as it builds them; stop after 6.
built_pipe = []
rows_pipe = schedule(100000, 12, 240, built_pipe)
first_six_pipe = list(islice(rows_pipe, 6))
rows_pipe.close()
print("pipelined: rows built for a query that reads 6:",
      sum(1 for b in built_pipe if b != "stopped"), "| then", built_pipe[-1])
same = first_six == first_six_pipe
print("same first six rows both ways:", same)

Output

Loan 100000 at 12% a year over 6 months, payment 17254.84
month  principal  interest    balance
    1   16254.84   1000.00   83745.16
    2   16417.39    837.45   67327.77
    3   16581.56    673.28   50746.21
    4   16747.38    507.46   33998.83
    5   16914.85    339.99   17083.98
    6   17083.98    170.84       0.00
total interest 3529.02 | last payment 17254.82
collection: rows built before month 1 reached the caller: 240
pipelined: rows built for a query that reads 6: 6 | then stopped
same first six rows both ways: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stream_model.py

Month 1 pays 1,000.00 of interest; month 6 pays 170.84 and clears the balance to 0.00, for 3,529.02 in all. For the 240-month loan the collection style built 240 rows before month 1 reached the caller; the streaming style built 6.

A rounding remainder in the last row

The same rounded payment every month leaves -0.02 after month 6 here. Clamping it to 0 makes the rows disagree with the money paid: let the last month repay exactly the balance left.

Checkpoint

Check: Rows handed over with PIPE ROW

  1. Question 1 of 2 What does PIPE ROW do inside emi_schedule?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It hands one row to the query and the loop carries on

    Each PIPE ROW sends one element of the declared collection type to the invoker without returning control, so emi_schedule pipes month 1, then builds and pipes month 2, and so on until its final RETURN.

  2. Question 2 of 2 Which return type can a pipelined table function declare?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A nested table type declared in a package, whose elements are NUMBER

    The return type is a collection type defined at schema level or inside a package, never an associative array, and its elements must be SQL types. emi_rows_t, a table of the object type emi_row_t, is one choice.

What the function sees when its query stops early

A query that reads numbered_rows(1000) with WHERE ROWNUM <= 3 needs 3 of the 1,000 rows. Once it has them, the PIPE ROW at which the function is paused raises the predefined exception NO_DATA_NEEDED: The predefined exception ORA-06548 that PIPE ROW raises in a pipelined table function when the query reading it needs no more rows. The query keeps its rows; a handler is needed only to release resources., ORA-06548, so the loop goes no further, and the query finishes normally with its three:

Interactive · stage by stage What happens when the query stops reading early Step through numbered_rows(1000) read by a query that needs only three rows.
What happens when the query stops reading early, step 5 of 5. Showing Rows go to the query, Query has 3 rows, PIPE ROW raises, Handler cleans up, Query ends normally. In focus: Query ends normally, trap.OTHERS errorsRows go to the queryhanded over in batchesQuery has 3 rowsROWNUM <= 3 is metPIPE ROW raisesORA-06548Handler cleans upthen RETURNQuery ends normallythree rows and no error

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Rows go to the query. numbered_rows pipes 1, 2 and 3, pausing at each PIPE ROW while the query takes the row. The runtime delivers piped rows in batches, so a function can also run a little ahead of what the query has read.
  2. The query needs no more. The query asked for ROWNUM <= 3. Once it holds three rows it needs nothing else from the function, which is still inside its loop with up to 997 rows to go.
  3. PIPE ROW raises the exception. The PIPE ROW at which the function is paused raises the predefined exception NO_DATA_NEEDED, ORA-06548: in the lab run, the one that handed over row 3. No more rows are built.
  4. The handler releases resources. The WHEN NO_DATA_NEEDED handler runs: this is where a function closes a cursor or a file it opened, then ends with RETURN. Without a handler the function simply ends here.
  5. The query is unaffected. Either way the query finishes with its three rows and no error. Only an OTHERS handler that catches the exception and raises another error turns this normal stop into a failed query.

Unhandled, the exception simply ends the function’s run; it never reaches the query. A handler matters when the function holds something to release, such as a cursor or a file it opened: it closes it and ends with RETURN (re-raising NO_DATA_NEEDED does the same). Oracle’s PIPE ROW page adds that piped rows can travel in batches, so a function may stop a few rows past the last one read. In the lab run below it stops at row 3, the last row the query read:

no_data_needed.sql: a full read, an early stop, and the OTHERS handler that breaks it SQL · pipeline/no_data_needed.sql
-- Engine: Oracle AI Database 26ai Free, Release 23.26.3.0.0; user LAB in FREEPDB1. Client: SQL*Plus or SQLcl.
-- What a pipelined function sees when the query that reads it stops early. PIPE ROW raises the predefined
-- exception NO_DATA_NEEDED (ORA-06548) once the query needs no more rows; the function cleans up in its handler.
-- The runtime hands piped rows to the query in batches, so the row where the function stops can be later than the
-- last row the query read: the DBMS_OUTPUT lines show where it stopped on your run.
SET FEEDBACK OFF
SET SERVEROUTPUT ON

DROP FUNCTION IF EXISTS numbered_rows;
DROP FUNCTION IF EXISTS numbered_rows_others;
DROP TYPE IF EXISTS row_numbers_t;

CREATE TYPE row_numbers_t AS TABLE OF NUMBER;
/

-- Handles NO_DATA_NEEDED: the place to close what the function opened (a cursor, a file, a session on a service).
CREATE FUNCTION numbered_rows (p_last IN NUMBER)
  RETURN row_numbers_t PIPELINED
IS
  v_built NUMBER := 0;
BEGIN
  DBMS_OUTPUT.PUT_LINE('numbered_rows: started');
  FOR i IN 1 .. p_last LOOP
    v_built := i;
    PIPE ROW (i);
  END LOOP;
  DBMS_OUTPUT.PUT_LINE('numbered_rows: all ' || v_built || ' rows piped, clean-up done');
  RETURN;
EXCEPTION
  WHEN NO_DATA_NEEDED THEN
    DBMS_OUTPUT.PUT_LINE('numbered_rows: stopped while piping row ' || v_built || ', clean-up done');
    RETURN;
END numbered_rows;
/

-- The trap: an OTHERS handler with no NO_DATA_NEEDED handler before it turns the early stop into an error.
CREATE FUNCTION numbered_rows_others (p_last IN NUMBER)
  RETURN row_numbers_t PIPELINED
IS
BEGIN
  FOR i IN 1 .. p_last LOOP
    PIPE ROW (i);
  END LOOP;
  RETURN;
EXCEPTION
  WHEN OTHERS THEN
    RAISE_APPLICATION_ERROR(-20001, 'numbered_rows_others failed: ' || SQLERRM);
END numbered_rows_others;
/

SET FEEDBACK ON
-- 1. The query reads every row. Expected: 1 to 5, then "all 5 rows piped, clean-up done".
SELECT COLUMN_VALUE AS n FROM numbered_rows(5);

-- 2. The query needs three of 1,000 rows. Expected: 1, 2, 3, then, in the lab, "stopped while piping row 3": the
--    PIPE ROW that handed over row 3 raised NO_DATA_NEEDED once the query had its three rows.
SELECT COLUMN_VALUE AS n FROM numbered_rows(1000) WHERE ROWNUM <= 3;

-- 3. The same early stop through the OTHERS handler. Expected: no rows and ORA-20001 that names ORA-06548.
SELECT COLUMN_VALUE AS n FROM numbered_rows_others(1000) WHERE ROWNUM <= 3;

-- The fix for 3: a NO_DATA_NEEDED handler BEFORE the OTHERS handler.
CREATE OR REPLACE FUNCTION numbered_rows_others (p_last IN NUMBER)
  RETURN row_numbers_t PIPELINED
IS
BEGIN
  FOR i IN 1 .. p_last LOOP
    PIPE ROW (i);
  END LOOP;
  RETURN;
EXCEPTION
  WHEN NO_DATA_NEEDED THEN
    RETURN;                              -- expected: the query has what it needs
  WHEN OTHERS THEN
    RAISE_APPLICATION_ERROR(-20001, 'numbered_rows_others failed: ' || SQLERRM);
END numbered_rows_others;
/

-- 4. Expected now: 1, 2, 3 and no error.
SELECT COLUMN_VALUE AS n FROM numbered_rows_others(1000) WHERE ROWNUM <= 3;

DROP FUNCTION numbered_rows;
DROP FUNCTION numbered_rows_others;
DROP TYPE row_numbers_t;
SET SERVEROUTPUT OFF
no_data_needed.sql, run in the lab Python · no_data_needed_session.py
"""no_data_needed.sql in the lab: SQL*Plus runs it as LAB on a freshly loaded practice schema."""

from oracle_session import Lab

with Lab() as lab:
    lab.sqlplus("pipeline/no_data_needed.sql", errors=1)

Output

$ sqlplus -s lab@//localhost/FREEPDB1 @pipeline/no_data_needed.sql
         N
----------
         1
         2
         3
         4
         5

5 rows selected.

numbered_rows: started
numbered_rows: all 5 rows piped, clean-up done

         N
----------
         1
         2
         3

3 rows selected.

numbered_rows: started
numbered_rows: stopped while piping row 3, clean-up done
ERROR:
ORA-20001: numbered_rows_others failed: ORA-06548: no more rows needed
ORA-06512: at "LAB.NUMBERED_ROWS_OTHERS", line 11



no rows selected


Function created.


         N
----------
         1
         2
         3

3 rows selected.


Function dropped.


Function dropped.


Type dropped.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_data_needed_session.py

Query 3 is the failure to remember: it looks like a bug in the query, not in the handler.

An OTHERS handler that sees the stop as a failure

WHEN OTHERS THEN RAISE_APPLICATION_ERROR(...) catches NO_DATA_NEEDED too, so the three-row query fails with ORA-20001. Put WHEN NO_DATA_NEEDED THEN RETURN; before WHEN OTHERS, as query 4 does.

Checkpoint

Check: NO_DATA_NEEDED when a query stops early

  1. Question 1 of 2 numbered_rows(1000) has no exception handler. A query reads it with WHERE ROWNUM <= 3. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The query returns 3 rows and no error; the function's run simply ends

    When the query needs no more rows, PIPE ROW raises NO_DATA_NEEDED. Unhandled, it ends the function's run while the SELECT finishes normally with its three rows.

  2. Question 2 of 2 A pipelined function ends with EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20001, SQLERRM). Short reads of it started failing with ORA-20001. What is the smallest correct fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Add WHEN NO_DATA_NEEDED THEN RETURN; before WHEN OTHERS

    OTHERS catches NO_DATA_NEEDED too and turns a normal stop into an error. A NO_DATA_NEEDED handler first, ending with RETURN (or RAISE), keeps the stop quiet and leaves OTHERS for real failures.

Pipelined function, table SQL macro or polymorphic table function

SQL macros are another way to put PL/SQL behind a FROM clause. A table SQL macro: A PL/SQL function marked SQL_MACRO(TABLE) that returns the text of a query; Oracle uses that query in place of the call in the FROM clause, so no PL/SQL runs for each row. returns the text of a query, and Oracle uses that query in place of the call. With SQL macros no PL/SQL runs per row: the optimizer reads the returned query as if it were written into the statement; the scalar kind, for expressions, belongs to the lesson on PL/SQL functions called from SQL. A polymorphic table function: A table function that takes a whole table as an argument and decides its result columns from that table, so one function can serve many different tables. takes a whole table as an argument and decides its result columns from it. A pipelined function marked PARALLEL_ENABLE lets a parallel query split its input rows across server processes.

Compared Pipelined table function Table SQL macro Polymorphic table function
Rows come from PL/SQL builds them Tables the text names The table passed in
PL/SQL per row Yes None Only to add values
Fits A schedule, a series A query with parameters Columns decided per table

The schedule is the first column’s case: its rows exist nowhere until the function computes them. A report of the orders in one status is the second column’s: every value it shows already sits in a table.

Checkpoint

Check: Choosing the kind of table function

  1. One question A report lists the practice orders in one status, with customer, item and amount. Which tool fits it?

    Choose one answer.

    Show the answer: One question

    Answer: A table SQL macro

    A table SQL macro returns the query text and Oracle uses that query in place of the call, so the optimizer reads the tables directly. A pipelined function is for rows that only PL/SQL can compute.

Table SQL macros: the query the optimizer reads instead of the call Pro: opens with a Pro, Premium or Ultimate pass

A table SQL macro turns a reusable query with parameters into something a FROM clause can call, while the optimizer still sees every table and join. This part builds one on the practice orders, reads its plan, and sets out the rules a macro must keep.

In this part:

  • What Oracle puts in place of the macro call
  • The rules a table macro has to keep
  • One orders report written three ways, and what each costs

2 runnable examples (Python, SQL) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Polymorphic table functions and pipelined functions in parallel Pro: opens with a Pro, Premium or Ultimate pass

Two tools handle the cases the first part leaves open: a table whose columns are not known when the function is written, and a pipelined function fed so many rows that one process is too slow. This part builds a polymorphic table function on the practice tables and sets out what a parallel pipelined function needs.

In this part:

  • A polymorphic table function that hides columns from any table
  • What a pipelined function needs before it can run in parallel
  • Chaining pipelined functions, and the limits of their results

1 diagram · 2 runnable examples (Python, SQL) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise

The questions use the schedule and the three-row query:

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 In stream_model.py, a query reads only the first 6 rows of the 240-month schedule. How many rows did the collection-returning style build before month 1 reached the caller?

    Type a number.

    Show the answer: Question 1 of 6

    Answer: 240 rows

    A function that returns the whole collection builds every row first: the model printed 240. The generator, like a pipelined function, built 6 before it was closed.

  2. Question 2 of 6 Every month of emi_schedule repays 17,254.84 minus the interest. What goes wrong in month 6 on this loan?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: The balance ends at -0.02 unless the last month repays exactly what is left

    With 17,254.84 paid six times, the balance would end at -0.02. Letting month 6 repay the remaining 17,083.98 makes its payment 17,254.82 and the balance exactly 0.00.

  3. Question 3 of 6 Which exception does PIPE ROW raise when the query reading the function needs no more rows?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: NO_DATA_NEEDED

    NO_DATA_NEEDED (ORA-06548) is raised by PIPE ROW once the invoker needs no more rows.

  4. Question 4 of 6 Which pipelined function needs a NO_DATA_NEEDED handler?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: One that opened a cursor or a file it must close

    A handler matters when the function holds something to release; otherwise the early stop simply ends its run. Either way the invoking query keeps its rows.

  5. Question 5 of 6 A report needs one row for each of the next 90 days, with that day's planned interest. Which tool fits?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: A pipelined function that computes and pipes each day's row

    Generated rows, such as schedules and series, are what pipelined functions are for; queries over rows that already sit in tables suit a table SQL macro or a view.

  6. Question 6 of 6 In the comparison table, what decides between a pipelined function and a table SQL macro?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Whether the rows already sit in tables or must be computed

    The table's first row, "Rows come from", carries the decision: PL/SQL builds them, or tables already hold them.

Interview questions

How does a pipelined table function differ from a table function that returns a whole collection? (warm-up; fresher, 2–5 years)

A collection-returning function fills a nested table or varray with every row and returns it when its body ends, so the query waits for the last row and the whole collection sits in memory. A pipelined function hands each row over with PIPE ROW as soon as it is built, so early rows arrive early and memory holds only the rows in flight. When the query needs no more, PIPE ROW raises NO_DATA_NEEDED and the building stops.

Three longer questions follow, on tax lines, an open cursor and hidden columns.

  1. Mid to senior

    A billing report must turn each of about 2 million monthly invoices into tax lines: one CGST and one SGST line for a sale within a state, one IGST line for a sale across states. A colleague proposes a pipelined function that reads the invoices through a REF CURSOR. Would you build it that way? Defend your choice.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A package declares a cursor over a large table; its pipelined function opens the cursor, pipes transformed rows and closes it after the loop. Reports read the first 20 rows. The second report in a session fails with ORA-06511, cursor already open. Explain the cause, the fix and how you would prove it.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    Exports of about 40 tables must leave out every column whose name ends in _PHONE or _EMAIL, and new tables keep arriving. A teammate suggests one hand-written view per table. Would you use views, a table SQL macro or a polymorphic table function? Defend the choice and say what the export query must pass as literals.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does a pipelined table function gain over one that returns a collection?

    It hands each row to the query with PIPE ROW as soon as it is built, so early rows arrive early and the work stops once the query has enough. A collection-returning function builds all 240 rows of a schedule before the query sees month 1.

  2. What does a pipelined function's header and return type look like?

    RETURN <collection type> PIPELINED, with the collection type declared at schema level or in a package and with SQL element types, never an associative array. PIPE ROW sends one element without giving up control, and the final RETURN carries no value.

  3. What happens when the query stops reading a pipelined function early?

    The PIPE ROW at which the function is paused raises the predefined exception NO_DATA_NEEDED, ORA-06548, so the function builds no more rows, and the query finishes normally with the rows it has. Unhandled, the exception simply ends the function's run and never reaches the query.

  4. When does NO_DATA_NEEDED need a handler?

    When the function holds something to release, such as a cursor or a file it opened: the handler closes it and ends with RETURN, and re-raising the exception does the same.

  5. Why can a WHEN OTHERS handler break a query that reads only three rows?

    It catches NO_DATA_NEEDED too, so WHEN OTHERS THEN RAISE_APPLICATION_ERROR turns the normal early stop into ORA-20001, which looks like a bug in the query. Put WHEN NO_DATA_NEEDED THEN RETURN; before WHEN OTHERS.

  6. Why may a pipelined function have built rows the query never read?

    The runtime hands piped rows over in batches, so the function can be a few rows past the last one the query read when it stops.

  7. What does a table SQL macro do instead?

    Its body returns a query as text, and Oracle substitutes that query for the call, so the optimizer reads it as if it had been written into the statement and nothing runs in PL/SQL row by row.

  8. Which of the three fits which job?

    A pipelined table function for rows PL/SQL computes, such as a schedule or a series; a table SQL macro for a parameterised query whose values already sit in tables; a polymorphic table function, which takes a whole table as an argument, when the result columns are decided per table. PARALLEL_ENABLE lets a parallel query split a pipelined function's input.

Key takeaways

  • A pipelined table function hands each row to the query with PIPE ROW as soon as it is built.
  • Its return type is a schema-level or package collection type with SQL element types; its RETURN has no value.
  • When the query needs no more rows, PIPE ROW raises NO_DATA_NEEDED; a handler is needed only to release resources, and it must come before any OTHERS handler.
  • Computed rows suit a pipelined function, rows in tables a table SQL macro, and table-dependent columns a polymorphic table function.

Terms in this lesson

NO_DATA_NEEDED
The predefined exception ORA-06548 that PIPE ROW raises in a pipelined table function when the query reading it needs no more rows. The query keeps its rows; a handler is needed only to release resources.
pipelined table function
A PL/SQL table function declared PIPELINED that hands each row to the query with PIPE ROW as soon as it is built, instead of building a whole collection and returning it at the end.
polymorphic table function
A table function that takes a whole table as an argument and decides its result columns from that table, so one function can serve many different tables.
table SQL macro
A PL/SQL function marked SQL_MACRO(TABLE) that returns the text of a query; Oracle uses that query in place of the call in the FROM clause, so no PL/SQL runs for each row.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.