Search This Blog

Thursday 7 June 2012

Oracle UTL_FILE with Examples


Definition:
In Oracle PL/SQL, UTL_FILE is an Oracle supplied package which is used for file operations (read and write) in conjunction with the underlying operating system. UTL_FILE works for both server and client machine systems. A directory has to be created on the server, which points to the target file. For the files located on the server machine, the actual path can be given while creating the directory. For the files which are located on the client machines, however, the relative path is required along with the client machine name. In addition, the relative file path must be in shared mode with read and write access for the required users. A DBA must create the directory and then grant Read/Write access to the required users.

In earlier versions of Oracle, the parameter UTL_FILE_DIR was used to specify the file location path. This parameter is now deprecated and directory creation method is recommended.

UTL_FILE subprograms are listed as below.

  • FCLOSE - Closes a file.

  • FCLOSE_ALL - Closes all open file handles

  • FCOPY - Copies a contiguous portion of a file to a newly created file. Takes the following parameters:
    src_location, src_filename, dest_location, dest_filename, start_line, and end_line.

  • FFLUSH - Physically writes all pending output to a file.

  • FGETATTR - Reads and returns the attributes of a disk file. Returns the following items about the file:
    location, filename, fexists (a boolean), file_length (in bytes), and block_size.
    The location must be either an existing directory on the server AND be in the utl_file_dir parameter, or it may be a directory.

  • FGETPOS - Returns the current relative offset position within a file, in bytes as a binary_integer.

  • FOPEN - Opens a file for input or output. FOPEN takes the following parameters:
    the file location, the filename, the open_mode and the max_linesize.

  • FOPEN_NCHAR - Opens a file in Unicode for input or output.

  • FREMOVE - Deletes a disk file, assuming that you have sufficient privileges. Takes the following parameters:
    location and filename

  • FRENAME - Renames an existing file to a new name, similar to the UNIX mv function. FRENAME takes the following parameters:
    the src_location, the src_filename, the dest_location, the dest_filename, and overwrite (a boolean). The overwrite parameter determines whether or not the file, if it already exists, will be overwritten.

  • FSEEK - Adjusts the file pointer forward or backward within the file by the number of bytes specified. FSEEK takes the following parameters:
    the file, the absolute_offset (a binary_integer), and the relative_offset (a binary_integer).

  • GET_LINE - Reads text from an open file. GET_LINE takes the following parameters:
    the file (record), buffer (varchar2), and len (a binary_integer).

  • GET_LINE_NCHAR - Reads text in Unicode from an open file. GET_LINE_NCHAR takes the following parameters:
    the file (record), buffer (nvarchar2), and len (a binary_integer).

  • GET_RAW - Reads a RAW string value from a file and adjusts the file pointer ahead by the number of bytes read. GET_RAW takes the following parameters:
    file (record), buffer (raw), len (a binary_integer)

  • IS_OPEN - Determines if a file handle refers to an open file.

  • NEW_LINE - Writes one or more operating system-specific line terminators to a file. NEW_LINE takes the following parameters:
    file (record), lines (a binary_integer).

  • PUT - Writes a string to a file. PUT takes the following parameters:
    file (record), buffer (a varchar2).

  • PUT_LINE - Writes a line to a file, and also appends an operating system-specific line terminator. If a line was already written, it starts the line with CR/LF. This implies that the file, when being written into, does not end with CR/LF. In Oracle 9i the maximum line length that can be written is 32K. PUT_LINE takes the following parameters:
    file (record), buffer (a varchar2).

  • PUT_LINE_NCHAR - Writes a Unicode line to a file. PUT_LINE takes the following parameters:
    file (record), buffer (a nvarchar2), autoflush (a boolean).

  • PUT_NCHAR - Writes a Unicode string to a file. PUT takes the following parameters:
    file (record), buffer (an nvarchar2).

  • PUTF - A PUT procedure with formatting.

  • PUTF_NCHAR - A PUT_NCHAR procedure with formatting, and writes a Unicode string to a file, with formatting.

  • PUT_RAW - Accepts as input a RAW data value and writes the value to the output buffer.

Example Usage:

In the below example, SYSDBA creates a directory MYDIR and grants R/W access to the user SCOTT. The user then creates a text file in the directory and writes a text into it.

--------------SYSDBA-----------------------

SQL> CREATE DIRECTORY MYDIR AS 'C:\TESTLOC';

Directory created.



SQL> GRANT READ, WRITE ON DIRECTORY MYDIR TO SCOTT

Grant succeeded.



--------------SCOTT-------------------------

 

DECLARE

  L_HANDLER UTL_FILE.FILE_TYPE;

BEGIN

  L_HANDLER := UTL_FILE.FOPEN('MYDIR', 'SYS.txt', 'W');

  UTL_FILE.PUTF(L_HANDLER, 'UTL_FILE write mode demonstration');

  UTL_FILE.FCLOSE(L_HANDLER);

END;



PL/SQL procedure successfully completed.

Note: O/S permissions are those of the user 'Oracle' ... not the schema owner or connected user
Source {ORACLE_HOME}/rdbms/admin/utlfile.sql
First Availability 7.3.4
Data Types -- file handle used in the block declaration section
TYPE file_type IS RECORD (
id BINARY_INTEGER, datatype BINARY_INTEGER, byte_mode BOOLEAN);

Dependencies
SELECT name FROM dba_dependencies WHERE referenced_name = 'UTL_FILE'
UNION
SELECT referenced_name FROM dba_dependencies WHERE name = 'UTL_FILE');

Exceptions
Exception Name Error Code Reason
access_denied 29289 Access to the file has been denied by the operating system
charsetmismatch 29298 A file is opened using FOPEN_NCHAR, but later I/O operations use nonchar functions such as PUTF or GET_LINE
delete_failed 29291 Unable to delete file
file_open ? File is already open
internal_error 29286 Unhandled internal error in the UTL_FILE package
invalid_filehandle 29282 File handle does not exist
invalid_filename 29288 A file with the specified name does not exist in the path
invalid_maxlinesize 29287 The MAX_LINESIZE value for FOPEN() is invalid; it should be within the range 1 to 32767
invalid_mode 29281 The open_mode parameter in FOPEN is invalid
invalid_offset 29290 The ABSOLUTE_OFFSET parameter for FSEEK() is invalid; it should be greater than 0 and less than the total number of bytes in the file
invalid_operation 29283 File could not be opened or operated on as requested
invalid_path 29280 Specified path does not exist or is not visible to Oracle
read_error 29284 Unable to read file
rename_failed 29292 Unable to rename file
write_error 29285 Unable to write to file
init.ora Parameters (deprecated) utl_file_dir=<directory_path_and_name>
utl_file_dir=c:\oraload
utl_file_dir=c: emp
utl_file_dir=*

Open Modes
A Append Text
AB Append Byte Mode
R Read Text
RB Read Byte Mode
W Write Text
WB Write Byte Mode
Security Model Execute is granted to PUBLIC which is a security risk. It is recommended that this grant be revoked immediately following installation.
conn / as sysdba

REVOKE execute ON utl_file FROM public;
 
Demo Setup
O/S Directory Creation mkdir c:\oraload
Oracle Directory Creation CREATE DIRECTORY oraload AS 'c:\oraload\';

GRANT READ,WRITE ON DIRECTORY oraload TO UWCLASS;
File To Create: test.txt Daniel,Morgan
Jack,Cline
 
FCLOSE
Close named file utl_file.fclose(file IN OUT file_type);
see FOPEN demo
 
FCLOSE_ALL

Close all files
utl_file.fclose_all;
set serveroutput on

DECLARE
 vInHandle  utl_file.file_type;
 vOutHandle utl_file.file_type;
BEGIN
  vInHandle := utl_file.fopen('ORALOAD', 'test.txt', 'R');
  vOutHandle := utl_file.fopen('ORALOAD', 'out.txt', 'W');
 
  IF utl_file.is_open(vInHandle) THEN
    utl_file.fclose_all;
    dbms_output.put_line('Closed All');
  END IF;
END fopen;
/
 
FCOPY

Copies a contiguous portion of a file to a newly created file
utl_file.fcopy(
src_location  IN VARCHAR2,
src_filename  IN VARCHAR2,
dest_location IN VARCHAR2,
dest_filename IN VARCHAR2,
start_line    IN BINARY_INTEGER DEFAULT 1,
end_line      IN BINARY_INTEGER DEFAULT NULL);
BEGIN
  utl_file.fcopy('ORALOAD', 'test.txt', 'ORALOAD', 'dump.txt');
END;
/
 
FFLUSH
Physically writes pending data to the file identified by the file handle utl_file.fflush(file IN file_type);
See WRITE Demo Below
 
FGETATTR

Reads and returns the attributes of a disk file
utl_file.fgetattr(
location    IN  VARCHAR2,
filename    IN  VARCHAR2,
fexists     OUT BOOLEAN,
file_length OUT NUMBER,
block_size  OUT BINARY_INTEGER);
set serveroutput on

DECLARE
 ex    BOOLEAN;
 flen  NUMBER;
 bsize NUMBER;
BEGIN
  utl_file.fgetattr('ORALOAD', 'test.txt', ex, flen, bsize);

  IF ex THEN
    dbms_output.put_line('File Exists');
  ELSE
    dbms_output.put_line('File Does Not Exist');
  END IF;
  dbms_output.put_line('File Length: ' || TO_CHAR(flen));
  dbms_output.put_line('Block Size: ' || TO_CHAR(bsize));
END fgetattr;
/
 
FGETPOS
Returns the current relative offset position within a file, in bytes utl_file.fgetpos(file IN file_type) RETURN BINARY_INTEGER;
See READ_WRITE Demo Below
 
FOPEN

Open a file for read operations
utl_file.fopen(
file_location IN VARCHAR2,
file_name     IN VARCHAR2,
open_mode     IN VARCHAR2,
max_linesize  IN BINARY_INTEGER DEFAULT NULL)
RETURN file_type;
DECLARE
 vInHandle utl_file.file_type;
 vNewLine  VARCHAR2(250);
BEGIN
  vInHandle := utl_file.fopen('ORALOAD', 'test.txt', 'R');
  LOOP
    BEGIN
      utl_file.get_line(vInHandle, vNewLine);
      dbms_output.put_line(vNewLine);
    EXCEPTION
      WHEN OTHERS THEN
        EXIT;
    END;
  END LOOP;
  utl_file.fclose(vInHandle);
END fopen;
/
Open A File For Write Operations <file_handle> := utl_file.fopen(<file_location, file_name, 'W')
 
FOPEN_NCHAR
Open a file for multibyte characters

Note: since NCHAR contains mutibyte character, it is recommended that the max_linesize be less than 6400.
utl_file.fremove(
location     IN VARCHAR2,
filename     IN VARCHAR2,
open_mode    IN VARCHAR2,
max_linesize IN BINARY_INTEGER DEFAULT NULL)
RETURN file_type;
TBD
 
FREMOVE

Delete An Operating System File
utl_file.fremove(location IN VARCHAR2, filename IN VARCHAR2);
-- dump.txt is created in the FCOPY demo
BEGIN
  utl_file.fremove('ORALOAD', 'dump.txt');
END fremove;
/
 
FRENAME

Rename An Operating System File
utl_file.frename (
src_location  IN VARCHAR2,
src_filename  IN VARCHAR2,
dest_location IN VARCHAR2,
dest_filename IN VARCHAR2,
overwrite     IN BOOLEAN DEFAULT FALSE);
BEGIN
  utl_file.frename('ORALOAD','test.txt','ORALOAD','x.txt',TRUE);
END frename;
/
 
FSEEK
Adjusts the file pointer forward or backward within the file by the number of bytes specified utl_file.fseek(
file            IN OUT file_type,
absolute_offset IN     BINARY_INTEGER DEFAULT NULL,
relative_offset IN     BINARY_INTEGER DEFAULT NULL);
See Read-Write demo
 
GETLINE

Read a Line from a file
utl_file.getline(
file   IN  FILE_TYPE,
buffer OUT VARCHAR2,
len    IN  BINARY_INTEGER DEFAULT NULL);
See Read demos
 
GETLINE_NCHAR

Read a line from a file containing multi-byte characters
utl_file.getline_nchar(
file    IN  FILE_TYPE,
buffer  OUT NVARCHAR2,
len     IN  BINARY_INTEGER DEFAULT NULL);
See Read demos
 
GET_RAW
Reads a RAW string value from a file and adjusts the file pointer ahead by the number of bytes read utl_file.get_raw(file   IN  file_type,
buffer OUT NOCOPY RAW,
len    IN  BINARY_INTEGER DEFAULT NULL);
See UTL_MAIL demo
 
IS_OPEN
Returns True If A File Handle Is Open: Otherwise False utl_file.is_open(file IN FILE_TYPE) RETURN BOOLEAN;
See FCLOSE_ALL Demo
 
NEW_LINE
Writes one or more operating system-specific line terminators to a file utl_file.new_line(file IN FILE_TYPE, lines IN NATURAL := 1);
See Read Demo
 
PUT
Writes a string to a file utl_file.put(file IN FILE_TYPE, buffer IN VARCHAR2);
See WRITE Demo Below
 
PUTF

A PUT procedure with formatting
utl_file.putf(
file   IN file_type,
format IN VARCHAR2,
arg1   IN VARCHAR2 DEFAULT NULL,
arg2   IN VARCHAR2 DEFAULT NULL,
arg3   IN VARCHAR2 DEFAULT NULL,
arg4   IN VARCHAR2 DEFAULT NULL,
arg5   IN VARCHAR2 DEFAULT NULL);
See Write demo
 
PUT_LINE
Writes a line to a file.  Appends an operating system-specific line terminator utl_file.put_line(
file      IN FILE_TYPE,
buffer    IN VARCHAR2,
autoflush IN BOOLEAN DEFAULT FALSE);
See READ-WRITE Demo Below
 
PUT_NCHAR
Writes a Unicode string to a file utl_file.put_nchar(file IN file_type, buffer IN NVARCHAR2);
TBD
 
PUT_RAW

Accepts as input a RAW data value and writes the value to the output buffer
utl_file.put_raw(
file      IN file_type,
buffer    IN RAW,
autoflush IN BOOLEAN DEFAULT FALSE);
See EXTRACT_BLOB Demo Below
 
PUT_LINE_NCHAR
Writes a Unicode line to a file utl_file.put_line_nchar(file IN file_type, buffer IN NVARCHAR2);
TBD
 
PUTF_NCHAR

Writes a Unicode string to a file
utl_file.putf_nchar(
file   IN file_type,
format IN NVARCHAR2,
arg1   IN NVARCHAR2 DEFAULT NULL,
arg2   IN NVARCHAR2 DEFAULT NULL,
arg3   IN NVARCHAR2 DEFAULT NULL,
arg4   IN NVARCHAR2 DEFAULT NULL,
arg5   IN NVARCHAR2 DEFAULT NULL);
TBD
 
UTL_FILE Demos

Read Demo
create table test (
fld1 VARCHAR2(20),
fld2 VARCHAR2(20));

CREATE OR REPLACE PROCEDURE read_demo(file_name VARCHAR2) IS
 vSFile   utl_file.file_type;
 vNewLine VARCHAR2(200);
BEGIN
  vSFile := utl_file.fopen('ORALOAD', file_name,'r');

  IF utl_file.is_open(vSFile) THEN
    LOOP
      BEGIN
        utl_file.get_line(vSFile, vNewLine);

        IF vNewLine IS NULL THEN
          EXIT;
        END IF;

        INSERT INTO test
        (fld1, fld2)
        VALUES
        (vNewLine, file_name);
      EXCEPTION
        WHEN NO_DATA_FOUND THEN
          EXIT;
      END;
    END LOOP;
    COMMIT;
  END IF;
  utl_file.fclose(vSFile);
  utl_file.frename('ORALOAD', 'test.txt', 'ORALOAD', 'x.txt', TRUE);
EXCEPTION
  WHEN utl_file.invalid_mode THEN
    RAISE_APPLICATION_ERROR (-20051, 'Invalid Mode Parameter');
  WHEN utl_file.invalid_path THEN
    RAISE_APPLICATION_ERROR (-20052, 'Invalid File Location');
  WHEN utl_file.invalid_filehandle THEN
    RAISE_APPLICATION_ERROR (-20053, 'Invalid Filehandle');
  WHEN utl_file.invalid_operation THEN
    RAISE_APPLICATION_ERROR (-20054, 'Invalid Operation');
  WHEN utl_file.read_error THEN
    RAISE_APPLICATION_ERROR (-20055, 'Read Error');
  WHEN utl_file.internal_error THEN
    RAISE_APPLICATION_ERROR (-20057, 'Internal Error');
  WHEN utl_file.charsetmismatch THEN
    RAISE_APPLICATION_ERROR (-20058, 'Opened With FOPEN_NCHAR
    But Later I/O Inconsistent');
  WHEN utl_file.file_open THEN
    RAISE_APPLICATION_ERROR (-20059, 'File Already Opened');
  WHEN utl_file.invalid_maxlinesize THEN
    RAISE_APPLICATION_ERROR(-20060,'Line Size Exceeds 32K');
  WHEN utl_file.invalid_filename THEN
    RAISE_APPLICATION_ERROR (-20061, 'Invalid File Name');
  WHEN utl_file.access_denied THEN
    RAISE_APPLICATION_ERROR (-20062, 'File Access Denied By');
  WHEN utl_file.invalid_offset THEN
    RAISE_APPLICATION_ERROR (-20063,'FSEEK Param Less Than 0');
  WHEN OTHERS THEN
    RAISE_APPLICATION_ERROR (-20099, 'Unknown UTL_FILE Error');
END read_demo;
/

Read-Write Demo
CREATE OR REPLACE PROCEDURE rw_demo IS
 InFile   utl_file.file_type;
 OutFile  utl_file.file_type;
 vNewLine VARCHAR2(4000);
 i        PLS_INTEGER;
 j        PLS_INTEGER := 0;
 SeekFlag BOOLEAN := TRUE;
BEGIN
  -- open a file to read
  InFile := utl_file.fopen('ORALOAD', 'in.txt','r');
  -- open a file to write
  OutFile := utl_file.fopen('ORALOAD', 'out.txt', 'w');

  -- if the file to read was successfully opened
  IF utl_file.is_open(InFile) THEN
    -- loop through each line in the file
    LOOP
      BEGIN
        utl_file.get_line(InFile, vNewLine);

        i := utl_file.fgetpos(InFile);
        dbms_output.put_line(TO_CHAR(i));

        utl_file.put_line(OutFile, vNewLine, FALSE);
        utl_file.fflush(OutFile);

        IF SeekFlag = TRUE THEN
          utl_file.fseek(InFile, NULL, -30);
          SeekFlag := FALSE;
        END IF;
      EXCEPTION
        WHEN NO_DATA_FOUND THEN
          EXIT;
      END;
    END LOOP;
    COMMIT;
  END IF;
  utl_file.fclose(InFile);
  utl_file.fclose(OutFile);
EXCEPTION
  WHEN OTHERS THEN
    RAISE_APPLICATION_ERROR (-20099, 'Unknown UTL_FILE Error');
END rw_demo;
/

Write Demo

This demo writes out a Korn Shell script to run SQL*Loader
CREATE OR REPLACE PROCEDURE create_cmd_file AS
 CURSOR sll_cur IS
 SELECT loadname, loadfilename, loadfiledate
 FROM sqlldrlog
  WHERE run_status = 'B'
 ORDER BY sequenceno;

 sll_rec   sll_cur%ROWTYPE;

 DirLoc    VARCHAR2(30) := 'ORALOAD';
 LFileName sqlldrlog.loadfilename%TYPE;
 LFileDate sqlldrlog.loadfiledate%TYPE;
 ctl_file  VARCHAR2(500);
 dat_file  VARCHAR2(500);
 log_file  VARCHAR2(500);
 bad_file  VARCHAR2(500);
 Emsg      VARCHAR2(90) := 'Load CREATE_CMD_FILE Failed with ERROR ';
 vSubject := 'SQL Loader Failure Notification';
 DayFile  utl_file.file_type;
 LogFile  utl_file.file_type;
BEGIN
  DayFile := utl_file.fopen(DirLoc, 'execsqlldr.ksh','W');
  LogFile := utl_file.fopen(DirLoc, 'log_list.dat','W');

  OPEN sll_cur;
  LOOP
    FETCH sll_cur INTO sll_rec;
    EXIT WHEN sll_cur%NOTFOUND;

    ctl_file := '/data/cload/ctl/'|| LOWER(sll_rec.loadname) || '.ctl \';

    dat_file := '/data/cload/data/' || sll_rec.loadfilename || ' \';

    log_file := '/data/cload/log/' || LOWER(sll_rec.loadname) || '_' || TO_CHAR(sll_rec.loadfiledate, 'YYYYMMDD') || '.log \';

        bad_file := '/data/cload/bad/' || LOWER(sll_rec.loadname) || '_' || TO_CHAR(sll_rec.loadfiledate, 'YYYYMMDD') || '.bad';

    utl_file.putf(dayfile, 'sqlldr userid=%s control=%s data=%s log=%s bad=%s ', '/ \',
ctl_file, dat_file, log_file, bad_file);

    log_file := '/data/cload/log/' || LOWER(sll_rec.loadname) || '_' || TO_CHAR(sll_rec.loadfiledate, 'YYYYMMDD') || '.log';

    utl_file.putf(logfile,'%s ',log_file);
  END LOOP;
  utl_file.fclose(DayFile);
  utl_file.fclose(LogFile);
EXCEPTION
  WHEN utl_file.invalid_mode THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20051, Invalid Option';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.invalid_path THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20052, Invalid Path';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.invalid_filehandle THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20053, Invalid Filehandle';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.invalid_operation THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20054, Invalid Operation';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.read_error THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20055, Read Error';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.write_error THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20056, Write Error';
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
  WHEN utl_file.internal_error THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || '-20057, Internal Error';
  WHEN OTHERS THEN
    vErrMsg := SQLERRM;
    vMessage := Emsg || vErrMsg;
    sp_SendEmail (0, 'dba@psoug.org', vSubject, vMessage);
END create_cmd_file;
/

Extract BLOB Demo
CREATE OR REPLACE PROCEDURE blob2file(
pdname VARCHAR2, psname VARCHAR2, pfname VARCHAR2) IS

 vblob   BLOB;
 vstart  NUMBER := 1;
 bytelen NUMBER := 32000;
 len     NUMBER;
 my_vr   RAW(32000);
 x       NUMBER;
 l_output utl_file.file_type;
BEGIN
  -- define output directory
  l_output := utl_file.fopen('ORALOAD', pfname, 'WB', 32760);

  -- get length of blob
  SELECT dbms_lob.getlength(iblob)
  INTO len
  FROM pdm
  WHERE dname = pdname
  AND sname = psname
  AND fname = pfname;

  -- save blob length
  x := len;

  -- select blob into variable
  SELECT iblob
  INTO vblob
  FROM pdm
  WHERE dname = pdname
  AND sname = psname
  AND fname = pfname;

  -- if small enough for a single write
  IF len < 32760 THEN
    utl_file.put_raw(l_output,vblob);
    utl_file.fflush(l_output);
  ELSE -- write in pieces
    vstart := 1;
    WHILE vstart < len
    LOOP
      dbms_lob.read(vblob,bytelen,vstart,my_vr);

      utl_file.put_raw(l_output,my_vr);
      utl_file.fflush(l_output)

      -- set the start position for the next cut
      vstart := vstart + bytelen;

      -- set the end position if less than 32000 bytes
      x := x - bytelen;
      IF x < 32000 THEN
        bytelen := x;
      END IF;
    END LOOP;
  END IF;
  utl_file.fclose(l_output);
END blob2file;
/
 

Oracle Hints And how to Use Hints in Oracle


Oracle SQL Hints Tuning

Oracle hints are enclosed within comments to the SQL commands DELETE, SELECT or UPDATE or are designated by two dashes and a plus sign. To show the format the SELECT statement only will be used, but the format is identical for all three commands.


SELECT      /*+ hint --or-- text */    statement body  
            -- or --      
SELECT          --+ hint --or-- text      statement body



    • /*, */  -  These are the comment delimiters for multi-line comments
    • --  -  This is the comment delimiter for a single line comment (not usually used for hints)
    • +  -  This tells Oracle a hint follows, it must come immediately after the /*
    • hint  -  This is one of the allowed hints
    • text  -  This is the comment text

Oracle Hint
Meaning
+
Must be immediately after comment indicator, tells Oracle this is a list of hints.
ALL_ROWS
Use the cost based approach for best throughput.
CHOOSE
Default, if statistics are available will use cost, if not, rule.
FIRST_ROWS
Use the cost based approach for best response time.
RULE
Use rules based approach; this cancels any other hints specified for this statement.
Access Method Oracle Hints:

CLUSTER(table)
This tells Oracle to do a cluster scan to access the table.
FULL(table)
This tells the optimizer to do a full scan of the specified table.
HASH(table)
Tells Oracle to explicitly choose the hash access method for the table.
HASH_AJ(table)
Transforms a NOT IN subquery to a hash anti-join.
ROWID(table)
Forces a rowid scan of the specified table.
INDEX(table [index])
Forces an index scan of the specified table using the specified index(s). If a list of indexes is specified, the optimizer chooses the one with the lowest cost. If no index is specified then the optimizer chooses the available index for the table with the lowest cost.
INDEX_ASC (table [index])
Same as INDEX only performs an ascending search of the index chosen, this is functionally identical to the INDEX statement.
INDEX_DESC(table [index])
Same as INDEX except performs a descending search. If more than one table is accessed, this is ignored.
INDEX_COMBINE(table index)
Combines the bitmapped indexes on the table if the cost shows that to do so would give better performance.
INDEX_FFS(table index)
Perform a fast full index scan rather than a table scan.
MERGE_AJ (table)
Transforms a NOT IN subquery into a merge anti-join.
AND_EQUAL(table index index [index index index])
This hint causes a merge on several single column indexes. Two must be specified, five can be.
NL_AJ
Transforms a NOT IN subquery into a NL anti-join (nested loop).
HASH_SJ(t1, t2)
Inserted into the EXISTS subquery; This converts the subquery into a special type of hash join between t1 and t2 that preserves the semantics of the subquery. That is, even if there is more than one matching row in t2 for a row in t1, the row in t1 is returned only once.
MERGE_SJ (t1, t2)
Inserted into the EXISTS subquery; This converts the subquery into a special type of merge join between t1 and t2 that preserves the semantics of the subquery. That is, even if there is more than one matching row in t2 for a row in t1, the row in t1 is returned only once.
NL_SJ
Inserted into the EXISTS subquery; This converts the subquery into a special type of nested loop join between t1 and t2 that preserves the semantics of the subquery. That is, even if there is more than one matching row in t2 for a row in t1, the row in t1 is returned only once.
Oracle Hints for join orders and transformations:

ORDERED
This hint forces tables to be joined in the order specified. If you know table X has fewer rows, then ordering it first may speed execution in a join.
STAR
Forces the largest table to be joined last using a nested loops join on the index.
STAR_TRANSFORMATION
Makes the optimizer use the best plan in which a start transformation is used.
FACT(table)
When performing a star transformation use the specified table as a fact table.
NO_FACT(table)
When performing a star transformation do not use the specified table as a fact table.
PUSH_SUBQ
This causes nonmerged subqueries to be evaluated at the earliest possible point in the execution plan.
REWRITE(mview)
If possible forces the query to use the specified materialized view, if no materialized view is specified, the system chooses what it calculates is the appropriate view.
NOREWRITE
Turns off query rewrite for the statement, use it for when data returned must be concurrent and can't come from a materialized view.
USE_CONCAT
Forces combined OR conditions and IN processing in the WHERE clause to be transformed into a compound query using the UNION ALL set operator.
NO_MERGE (table)
This causes Oracle to join each specified table with another row source without a sort-merge join.
NO_EXPAND
 Prevents OR and IN processing expansion.
Oracle Hints for Join Operations:

USE_HASH (table)
 
This causes Oracle to join each specified table with another row source with a hash join.
USE_NL(table)
This operation forces a nested loop using the specified table as the controlling table.
USE_MERGE(table,[table, - ])
This operation forces a sort-merge-join operation of the specified tables.
DRIVING_SITE
The hint forces query execution to be done at a different site than that selected by Oracle. This hint can be used with either rule-based or cost-based optimization.
LEADING(table)
The hint causes Oracle to use the specified table as the first table in the join order.
Oracle Hints for Parallel Operations:

[NO]APPEND
This specifies that data is to be or not to be appended to the end of a file rather than into existing free space. Use only with INSERT commands.
NOPARALLEL (table
This specifies the operation is not to be done in parallel.
PARALLEL(table, instances)
This specifies the operation is to be done in parallel.
PARALLEL_INDEX
Allows parallelization of a fast full index scan on any index.
Other Oracle Hints:

CACHE
Specifies that the blocks retrieved for the table in the hint are placed at the most recently used end of the LRU list when the table is full table scanned.
NOCACHE
Specifies that the blocks retrieved for the table in the hint are placed at the least recently used end of the LRU list when the table is full table scanned.
[NO]APPEND
For insert operations will append (or not append) data at the HWM of table.
UNNEST
Turns on the UNNEST_SUBQUERY option for statement if UNNEST_SUBQUERY parameter is set to FALSE.
NO_UNNEST
Turns off the UNNEST_SUBQUERY option for statement if UNNEST_SUBQUERY parameter is set to TRUE.
PUSH_PRED
 Pushes the join predicate into the view.

As you can see, a dilemma with a stubborn index can be easily solved using FULL or NO_INDEX Oracle hints. You must know the application to be tuned. The DBA can provide guidance to developers but in all but the smallest development projects, it will be nearly impossible for a DBA to know everything about each application. It is clear that responsibility for application tuning rests solely on the developer's shoulders with help and guidance from the DBA. 

Using Global Hints

While Oracle hints normally refer to table in the query it is possible to specify a hint for a table within a view through the use of what are known as Oracle GLOBAL HINTS. This is done using the Oracle global hint syntax. Any table hint can be transformed into an Oracle global hint.
The syntax is:

/*+ hint(view_name.table_in_view) */

For example:

 /*+ full(sales_totals_vw.s_customer)*/

If the view is an inline view, place an alias on it and then use the alias to reference the inline view in the Oracle global hint.

Oracle Analytic Queries and Analytic Functions


How are analytic functions different from group or aggregate functions?



SELECT deptno,
COUNT(*) DEPT_COUNT
FROM emp
WHERE deptno IN (20, 30)
GROUP BY deptno;

DEPTNO                 DEPT_COUNT             
---------------------- ---------------------- 
20                     5                      
30                     6                      

2 rows selected

Consider the Query-1 and its result. Query-1 returns departments and their employee count. Most importantly it groups the records into departments in accordance with the GROUP BY clause. As such any non-"group by" column is not allowed in the select clause.

SELECT empno, deptno, 
COUNT(*) OVER (PARTITION BY 
deptno) DEPT_COUNT
FROM emp
WHERE deptno IN (20, 30);

     EMPNO     DEPTNO DEPT_COUNT
---------- ---------- ----------
      7369         20          5
      7566         20          5
      7788         20          5
      7902         20          5
      7876         20          5
      7499         30          6
      7900         30          6
      7844         30          6
      7698         30          6
      7654         30          6
      7521         30          6

11 rows selected.
Query-2

Now consider the analytic function query (Query-2) and its result. Note the repeating values of DEPT_COUNT column.
This brings out the main difference between aggregate and analytic functions. Though analytic functions give aggregate result they do not group the result set. They return the group value multiple times with each record. As such any other non-"group by" column or expression can be present in the select clause, for example, the column EMPNO in Query-2.
Analytic functions are computed after all joins, WHERE clause, GROUP BY and HAVING are computed on the query. The main ORDER BY clause of the query operates after the analytic functions. So analytic functions can only appear in the select list and in the main ORDER BY clause of the query.
In absence of any PARTITION or <window_clause> inside the OVER( ) portion, the function acts on entire record set returned by the where clause. Note the results of Query-3 and compare it with the result of aggregate function query Query-4.

SELECT empno, deptno, 
COUNT(*) OVER ( ) CNT
FROM emp
WHERE deptno IN (10, 20)
ORDER BY 2, 1;

     EMPNO     DEPTNO        CNT
---------- ---------- ----------
      7782         10          8
      7839         10          8
      7934         10          8
      7369         20          8
      7566         20          8
      7788         20          8
      7876         20          8
      7902         20          8
Query-3


SELECT COUNT(*) FROM emp
WHERE deptno IN (10, 20);

  COUNT(*)
----------
         8
Query-4


How to break the result set in groups or partitions?


It might be obvious from the previous example that the clause PARTITION BY is used to break the result set into groups. PARTITION BY can take any non-analytic SQL expression.
Some functions support the <window_clause> inside the partition to further limit the records they act on. In the absence of any <window_clause> analytic functions are computed on all the records of the partition clause.
The functions SUM, COUNT, AVG, MIN, MAX are the common analytic functions the result of which does not depend on the order of the records.
Functions like LEAD, LAG, RANK, DENSE_RANK, ROW_NUMBER, FIRST, FIRST VALUE, LAST, LAST VALUE depends on order of records. In the next example we will see how to specify that.

How to specify the order of the records in the partition?


The answer is simple, by the "ORDER BY" clause inside the OVER( ) clause. This is different from the ORDER BY clause of the main query which comes after WHERE. In this section we go ahead and introduce each of the very useful functions LEAD, LAG, RANK, DENSE_RANK, ROW_NUMBER, FIRST, FIRST VALUE, LAST, LAST VALUE and show how each depend on the order of the record.
The general syntax of specifying the ORDER BY clause in analytic function is:
ORDER BY <sql_expr> [ASC or DESC] NULLS [FIRST or LAST]
The syntax is self-explanatory.

ROW_NUMBER, RANK and DENSE_RANK


All the above three functions assign integer values to the rows depending on their order. That is the reason of clubbing them together.
ROW_NUMBER( ) gives a running serial number to a partition of records. It is very useful in reporting, especially in places where different partitions have their own serial numbers. In Query-5, the function ROW_NUMBER( ) is used to give separate sets of running serial to employees of departments 10 and 20 based on their HIREDATE.

SELECT empno, deptno, hiredate,
ROW_NUMBER( ) OVER (PARTITION BY
deptno ORDER BY hiredate
NULLS LAST) SRLNO
FROM emp
WHERE deptno IN (10, 20)
ORDER BY deptno, SRLNO;

EMPNO  DEPTNO HIREDATE       SRLNO
------ ------- --------- ----------
  7782      10 09-JUN-81          1
  7839      10 17-NOV-81          2
  7934      10 23-JAN-82          3
  7369      20 17-DEC-80          1
  7566      20 02-APR-81          2
  7902      20 03-DEC-81          3
  7788      20 09-DEC-82          4
  7876      20 12-JAN-83          5

8 rows selected.
Query-5 (ROW_NUMBER example)

RANK and DENSE_RANK both provide rank to the records based on some column value or expression. In case of a tie of 2 records at position N, RANK declares 2 positions N and skips position N+1 and gives position N+2 to the next record. While DENSE_RANK declares 2 positions N but does not skip position N+1.
Query-6 shows the usage of both RANK and DENSE_RANK. For DEPTNO 20 there are two contenders for the first position (EMPNO 7788 and 7902). Both RANK and DENSE_RANK declares them as joint toppers. RANK skips the next value that is 2 and next employee EMPNO 7566 is given the position 3. For DENSE_RANK there are no such gaps.

SELECT empno, deptno, sal,
RANK() OVER (PARTITION BY deptno
ORDER BY sal DESC NULLS LAST) RANK,
DENSE_RANK() OVER (PARTITION BY
deptno ORDER BY sal DESC NULLS
LAST) DENSE_RANK
FROM emp
WHERE deptno IN (10, 20)
ORDER BY 2, RANK;

EMPNO  DEPTNO   SAL  RANK DENSE_RANK
------ ------- ----- ----- ----------
  7839      10  5000     1          1
  7782      10  2450     2          2
  7934      10  1300     3          3
  7788      20  3000     1          1
  7902      20  3000     1          1
  7566      20  2975     3          2
  7876      20  1100     4          3
  7369      20   800     5          4

8 rows selected.
Query-6 (RANK and DENSE_RANK example)


LEAD and LAG


LEAD has the ability to compute an expression on the next rows (rows which are going to come after the current row) and return the value to the current row. The general syntax of LEAD is shown below:
LEAD (<sql_expr>, <offset>, <default>) OVER (<analytic_clause>)
<sql_expr> is the expression to compute from the leading row. <offset> is the index of the leading row relative to the current row. <offset> is a positive integer with default 1. <default> is the value to return if the <offset> points to a row outside the partition range.
The syntax of LAG is similar except that the offset for LAG goes into the previous rows. Query-7 and its result show simple usage of LAG and LEAD function.

SELECT deptno, empno, sal,
LEAD(sal, 1, 0) OVER (PARTITION BY dept ORDER BY sal DESC NULLS LAST) NEXT_LOWER_SAL,
LAG(sal, 1, 0) OVER (PARTITION BY dept ORDER BY sal DESC NULLS LAST) PREV_HIGHER_SAL
FROM emp
WHERE deptno IN (10, 20)
ORDER BY deptno, sal DESC;

 DEPTNO  EMPNO   SAL NEXT_LOWER_SAL PREV_HIGHER_SAL
------- ------ ----- -------------- ---------------
     10   7839  5000           2450               0
     10   7782  2450           1300            5000
     10   7934  1300              0            2450
     20   7788  3000           3000               0
     20   7902  3000           2975            3000
     20   7566  2975           1100            3000
     20   7876  1100            800            2975
     20   7369   800              0            1100

8 rows selected.
Query-7 (LEAD and LAG)


FIRST VALUE and LAST VALUE function


The general syntax is:
FIRST_VALUE(<sql_expr>) OVER (<analytic_clause>)
The FIRST_VALUE analytic function picks the first record from the partition after doing the ORDER BY. The <sql_expr> is computed on the columns of this first record and results are returned. The LAST_VALUE function is used in similar context except that it acts on the last record of the partition.

-- How many days after the first hire of each department were the next
-- employees hired?

SELECT empno, deptno, hiredate ? FIRST_VALUE(hiredate)
OVER (PARTITION BY deptno ORDER BY hiredate) DAY_GAP
FROM emp
WHERE deptno IN (20, 30)
ORDER BY deptno, DAY_GAP;

     EMPNO     DEPTNO    DAY_GAP
---------- ---------- ----------
      7369         20          0
      7566         20        106
      7902         20        351
      7788         20        722
      7876         20        756
      7499         30          0
      7521         30          2
      7698         30         70
      7844         30        200
      7654         30        220
      7900         30        286

11 rows selected.
Query-8 (FIRST_VALUE)


FIRST and LAST function


The FIRST function (or more properly KEEP FIRST function) is used in a very special situation. Suppose we rank a group of record and found several records in the first rank. Now we want to apply an aggregate function on the records of the first rank. KEEP FIRST enables that.
The general syntax is:
Function( ) KEEP (DENSE_RANK FIRST ORDER BY <expr>) OVER (<partitioning_clause>)
Please note that FIRST and LAST are the only functions that deviate from the general syntax of analytic functions. They do not have the ORDER BY inside the OVER clause. Neither do they support any <window> clause. The ranking done in FIRST and LAST is always DENSE_RANK. The query below shows the usage of FIRST function. The LAST function is used in similar context to perform computations on last ranked records.

-- How each employee's salary compare with the average salary of the first
-- year hires of their department?

SELECT empno, deptno, TO_CHAR(hiredate,'YYYY') HIRE_YR, sal,
TRUNC(
AVG(sal) KEEP (DENSE_RANK FIRST
ORDER BY TO_CHAR(hiredate,'YYYY') )
OVER (PARTITION BY deptno)
     ) AVG_SAL_YR1_HIRE
FROM emp
WHERE deptno IN (20, 10)
ORDER BY deptno, empno, HIRE_YR;

     EMPNO     DEPTNO HIRE        SAL AVG_SAL_YR1_HIRE
---------- ---------- ---- ---------- ----------------
      7782         10 1981       2450             3725
      7839         10 1981       5000             3725
      7934         10 1982       1300             3725
      7369         20 1980        800              800
      7566         20 1981       2975              800
      7788         20 1982       3000              800
      7876         20 1983       1100              800
      7902         20 1981       3000              800

8 rows selected.
Query-9 (KEEP FIRST)


How to specify the Window clause (ROW type or RANGE type windows)?


Some analytic functions (AVG, COUNT, FIRST_VALUE, LAST_VALUE, MAX, MIN and SUM among the ones we discussed) can take a window clause to further sub-partition the result and apply the analytic function. An important feature of the windowing clause is that it is dynamic in nature.
The general syntax of the <window_clause> is [ROW or RANGE] BETWEEN <start_expr> AND <end_expr>
<start_expr> can be any one of the following
  1. UNBOUNDED PECEDING
  2. CURRENT ROW
  3. <sql_expr> PRECEDING or FOLLOWING.

    <end_expr> can be any one of the following
  1. UNBOUNDED FOLLOWING or
  2. CURRENT ROW or
  3. <sql_expr> PRECEDING or FOLLOWING.
For ROW type windows the definition is in terms of row numbers before or after the current row. So for ROW type windows <sql_expr> must evaluate to a positive integer.
For RANGE type windows the definition is in terms of values before or after the current ORDER. We will take this up in details latter.
The ROW or RANGE window cannot appear together in one OVER clause. The window clause is defined in terms of the current row. But may or may not include the current row. The start point of the window and the end point of the window can finish before the current row or after the current row. Only start point cannot come after the end point of the window. In case any point of the window is undefined the default is UNBOUNDED PRECEDING for <start_expr> and UNBOUNDED FOLLOWING for <end_expr>.
If the end point is the current row, syntax only in terms of the start point can be can be [ROW or RANGE] [<start_expr> PRECEDING or UNBOUNDED PRECEDING ]
[ROW or RANGE] CURRENT ROW is also allowed but this is redundant. In this case the function behaves as a single-row function and acts only on the current row.

ROW Type Windows


For analytic functions with ROW type windows, the general syntax is:
Function( ) OVER (PARTITIN BY <expr1> ORDER BY <expr2,..> ROWS BETWEEN <start_expr> AND <end_expr>) or Function( ) OVER (PARTITON BY <expr1> ORDER BY <expr2,..> ROWS [<start_expr> PRECEDING or UNBOUNDED PRECEDING]
For ROW type windows the windowing clause is in terms of record numbers.
The query Query-10 has no apparent real life description (except column FROM_PU_C) but the various windowing clause are illustrated by a COUNT(*) function. The count simply shows the number of rows inside the window definition. Note the build up of the count for each column for the YEAR 1981.
The column FROM_P3_TO_F1 shows an example where start point of the window is before the current row and end point of the window is after current row. This is a 5 row window; it shows values less than 5 during the beginning and end.

-- The query below has no apparent real life description (except 
-- column FROM_PU_C) but is remarkable in illustrating the various windowing
-- clause by a COUNT(*) function.
 
SELECT empno, deptno, TO_CHAR(hiredate, 'YYYY') YEAR,
COUNT(*) OVER (PARTITION BY TO_CHAR(hiredate, 'YYYY')
ORDER BY hiredate ROWS BETWEEN 3 PRECEDING AND 1 FOLLOWING) FROM_P3_TO_F1,
COUNT(*) OVER (PARTITION BY TO_CHAR(hiredate, 'YYYY')
ORDER BY hiredate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) FROM_PU_TO_C,
COUNT(*) OVER (PARTITION BY TO_CHAR(hiredate, 'YYYY')
ORDER BY hiredate ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) FROM_P2_TO_P1,
COUNT(*) OVER (PARTITION BY TO_CHAR(hiredate, 'YYYY')
ORDER BY hiredate ROWS BETWEEN 1 FOLLOWING AND 3 FOLLOWING) FROM_F1_TO_F3
FROM emp
ORDEDR BY hiredate

 EMPNO  DEPTNO YEAR FROM_P3_TO_F1 FROM_PU_TO_C FROM_P2_TO_P1 FROM_F1_TO_F3
------ ------- ---- ------------- ------------ ------------- -------------
  7369      20 1980             1            1             0             0
  <font bgcolor=yellow>7499      30 1981             2            1             0             3
  7521      30 1981             3            2             1             3
  7566      20 1981             4            3             2             3
  7698      30 1981             5            4             3             3
  7782      10 1981             5            5             3             3
  7844      30 1981             5            6             3             3
  7654      30 1981             5            7             3             3
  7839      10 1981             5            8             3             2
  7900      30 1981             5            9             3             1
  7902      20 1981             4           10             3             0</font>
  7934      10 1982             2            1             0             1
  7788      20 1982             2            2             1             0
  7876      20 1983             1            1             0             0

14 rows selected.
Query-10 (ROW type windowing example)

The column FROM_PU_TO_CURR shows an example where start point of the window is before the current row and end point of the window is the current row. This column only has some real world significance. It can be thought of as the yearly employee build-up of the organization as each employee is getting hired.
The column FROM_P2_TO_P1 shows an example where start point of the window is before the current row and end point of the window is before the current row. This is a 3 row window and the count remains constant after it has got 3 previous rows.
The column FROM_F1_TO_F3 shows an example where start point of the window is after the current row and end point of the window is after the current row. This is a reverse of the previous column. Note how the count declines during the end.

RANGE Windows


For RANGE windows the general syntax is same as that of ROW:
Function( ) OVER (PARTITION BY <expr1> ORDER BY <expr2> RANGE BETWEEN <start_expr> AND <end_expr>) or Function( ) OVER (PARTITION BY <expr1> ORDER BY <expr2> RANGE [<start_expr> PRECEDING or UNBOUNDED PRECEDING]
For <start_expr> or <end_expr> we can use UNBOUNDED PECEDING, CURRENT ROW or <sql_expr> PRECEDING or FOLLOWING. However for RANGE type windows <sql_expr> must evaluate to value compatible with ORDER BY expression <expr1>.
<sql_expr> is a logical offset. It must be a constant or expression that evaluates to a positive numeric value or an interval literal. Only one ORDER BY expression is allowed.
If <sql_expr> evaluates to a numeric value, then the ORDER BY expr must be a NUMBER or DATE datatype. If <sql_expr> evaluates to an interval value, then the ORDER BY expr must be a DATE datatype.
Note the example (Query-11) below which uses RANGE windowing. The important thing here is that the size of the window in terms of the number of records can vary.

-- For each employee give the count of employees getting half more that their 
-- salary and also the count of employees in the departments 20 and 30 getting half 
-- less than their salary.
 
SELECT deptno, empno, sal,
Count(*) OVER (PARTITION BY deptno ORDER BY sal RANGE
BETWEEN UNBOUNDED PRECEDING AND (sal/2) PRECEDING) CNT_LT_HALF,
COUNT(*) OVER (PARTITION BY deptno ORDER BY sal RANGE
BETWEEN (sal/2) FOLLOWING AND UNBOUNDED FOLLOWING) CNT_MT_HALF
FROM emp
WHERE deptno IN (20, 30)
ORDER BY deptno, sal

 DEPTNO  EMPNO   SAL CNT_LT_HALF CNT_MT_HALF
------- ------ ----- ----------- -----------
     20   7369   800           0           3
     20   7876  1100           0           3
     20   7566  2975           2           0
     20   7788  3000           2           0
     20   7902  3000           2           0
     30   7900   950           0           3
     30   7521  1250           0           1
     30   7654  1250           0           1
     30   7844  1500           0           1
     30   7499  1600           0           1
     30   7698  2850           3           0

11 rows selected.
Query-11 (RANGE type windowing example)


Order of computation and performance tips


Defining the PARTITOIN BY and ORDER BY clauses on indexed columns (ordered in accordance with the PARTITION CLAUSE and then the ORDER BY clause in analytic function) will provide optimum performance. For Query-5, for example, a composite index on (deptno, hiredate) columns will prove effective.
It is advisable to always use CBO for queries using analytic functions. The tables and indexes should be analyzed and optimizer mode should be CHOOSE.
Even in absence of indexes analytic functions provide acceptable performance but need to do sorting for computing partition and order by clause. If the query contains multiple analytic functions, sorting and partitioning on two different columns should be avoided if they are both not indexed.

Conclusion


The aim of this article is not to make the reader try analytic functions forcibly in every other complex SQL. It is meant for a SQL coder, who has been avoiding analytic functions till now, even in complex analytic queries and reinventing the same feature much painstakingly by native SQL and join query. Its job is done if such a person finds analytic functions clear, understandable and usable after going through the article, and starts using them.