Column formatting was always a pain in sqlplus when writing queries on the prompt. Most people use tools like SQL Developer or Quest TOAD which can scroll horizontally when running queries against a database, but as a consultant you are often still forced to use sqlplus. Here’s the issue: When running e.g. a query on a table T1 (which is a copy of ALL_OBJECTS) it looks by default as follows and is hard to read:

cbleile@orcl@orcl> create table t1 as select * from all_objects;

Table created.

cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1 where rownum < 4;

OWNER                                                                                                                            O
-------------------------------------------------------------------------------------------------------------------------------- -
OBJECT_NAME
--------------------------------------------------------------------------------------------------------------------------------
SYS                                                                                                                              Y
TS$

SYS                                                                                                                              Y
ICOL$

SYS                                                                                                                              Y
C_FILE#_BLOCK#

The column width is defined by the maximum length of the data type. I.e. for a VARCHAR2(128) a column of width 128 is defined in the output (if the linessize is less than the column width then the linesize defines the maximum column width displayed).

You can format columns of course:

cbleile@orcl@orcl> column owner format a32
cbleile@orcl@orcl> column object_name format a32
cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1 where rownum < 4;

OWNER                            O OBJECT_NAME
-------------------------------- - --------------------------------
SYS                              Y TS$
SYS                              Y ICOL$
SYS                              Y C_FILE#_BLOCK#

But running lots of ad hoc queries in sqlplus is quite annoying if you have to format all columns manually.
This has been resolved in sqlcl by using “set sqlformat ansiconsole”:

oracle@oracle-19c6-vagrant:/home/oracle/ [orcl] alias sqlcl="bash $ORACLE_HOME/sqldeveloper/sqldeveloper/bin/sql"
oracle@oracle-19c6-vagrant:/home/oracle/ [orcl] sqlcl cbleile

SQLcl: Release 19.1 Production on Thu Oct 01 08:43:49 2020

Copyright (c) 1982, 2020, Oracle.  All rights reserved.

Password? (**********?) *******
Last Successful login time: Thu Oct 01 2020 08:43:51 +01:00

Connected to:
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production
Version 19.6.0.0.0


SQL> set sqlformat ansiconsole
SQL> select owner, oracle_maintained, object_name from t1 where rownum < 4;
OWNER   ORACLE_MAINTAINED   OBJECT_NAME      
------- ------------------- ----------------
SYS     Y                   TS$              
SYS     Y                   ICOL$            
SYS     Y                   C_FILE#_BLOCK#   

In sqlcl all rows up to “pagesize” are pre-fetched and the column-format is adjusted for the page to the maximum length per column. E.g.

SQL> set pagesize 1
SQL> select 'short' val from dual
  2  union all
  3  select 'looooooooooooooooooooooooooooooooooooooooooooooooooooooong' val from dual;
VAL     
-------
short   

VAL                                                          
------------------------------------------------------------
looooooooooooooooooooooooooooooooooooooooooooooooooooooong   

SQL> set pagesize 2
SQL> select 'short' val from dual
  2  union all
  3  select 'looooooooooooooooooooooooooooooooooooooooooooooooooooooong' val from dual;
VAL                                                          
------------------------------------------------------------
short   
looooooooooooooooooooooooooooooooooooooooooooooooooooooong   

REMARK: Due to the algorithm in sqlcl you can force sqlcl to crash with sqlformat ansiconsole if it does not have enough memory to pre-fetch the data for a single page. E.g. having lots of data returned and the maximum pagesize set (50000):

SQL> set sqlformat ansiconsole
SQL> set pagesize 50000
SQL> select 
  2  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' a,
  3  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' b,
  4  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' c,
  5  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' d,
....
315  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' y3,
316  'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' z3
317  from all_objects;
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.(String.java:207)
	at oracle.sql.CharacterSetUTF.toStringWithReplacement(CharacterSetUTF.java:134)
	at oracle.sql.CHAR.getStringWithReplacement(CHAR.java:307)
	at oracle.sql.CHAR.toString(CHAR.java:318)
	at oracle.sql.CHAR.stringValue(CHAR.java:411)
	at oracle.dbtools.raptor.nls.DefaultNLSProvider.format(DefaultNLSProvider.java:208)
	at oracle.dbtools.raptor.nls.OracleNLSProvider.format(OracleNLSProvider.java:214)
	at oracle.dbtools.raptor.utils.NLSUtils.format(NLSUtils.java:187)
	at oracle.dbtools.raptor.format.ANSIConsoleFormatter.printColumn(ANSIConsoleFormatter.java:149)
	at oracle.dbtools.raptor.format.ResultSetFormatterWrapper.print(ResultSetFormatterWrapper.java:274)
	at oracle.dbtools.raptor.format.ResultSetFormatterWrapper.print(ResultSetFormatterWrapper.java:222)
	at oracle.dbtools.raptor.format.ResultsFormatter.print(ResultsFormatter.java:518)
	at oracle.dbtools.db.ResultSetFormatter.formatResults(ResultSetFormatter.java:124)
	at oracle.dbtools.db.ResultSetFormatter.formatResults(ResultSetFormatter.java:70)
	at oracle.dbtools.raptor.newscriptrunner.SQL.processResultSet(SQL.java:798)
	at oracle.dbtools.raptor.newscriptrunner.SQL.executeQuery(SQL.java:709)
	at oracle.dbtools.raptor.newscriptrunner.SQL.run(SQL.java:83)
	at oracle.dbtools.raptor.newscriptrunner.ScriptRunner.runSQL(ScriptRunner.java:404)
	at oracle.dbtools.raptor.newscriptrunner.ScriptRunner.run(ScriptRunner.java:230)
	at oracle.dbtools.raptor.newscriptrunner.ScriptExecutor.run(ScriptExecutor.java:344)
	at oracle.dbtools.raptor.newscriptrunner.ScriptExecutor.run(ScriptExecutor.java:227)
	at oracle.dbtools.raptor.scriptrunner.cmdline.SqlCli.process(SqlCli.java:404)
	at oracle.dbtools.raptor.scriptrunner.cmdline.SqlCli.processLine(SqlCli.java:415)
	at oracle.dbtools.raptor.scriptrunner.cmdline.SqlCli.startSQLPlus(SqlCli.java:1249)
	at oracle.dbtools.raptor.scriptrunner.cmdline.SqlCli.main(SqlCli.java:491)
oracle@oracle-19c6-vagrant:/home/oracle/ [orcl] 

But back to sqlplus: To address the issue with the column-formatting several solutions were developed. E.g. Tom Kyte provided a procedure print_table over 20 years ago to list each column and its value in a separate line:

cbleile@orcl@orcl> exec print_table('select owner, oracle_maintained, object_name from t1 where rownum < 4');
OWNER                 : SYS
ORACLE_MAINTAINED     : Y
OBJECT_NAME           : TS$
-----------------
OWNER                 : SYS
ORACLE_MAINTAINED     : Y
OBJECT_NAME           : ICOL$
-----------------
OWNER                 : SYS
ORACLE_MAINTAINED     : Y
OBJECT_NAME           : C_FILE#_BLOCK#
-----------------

PL/SQL procedure successfully completed.

The same can be done with xmltable since more recent versions. See e.g. here.

That was perfect when querying a couple of rows.

Alternatively some people used a terminal emulation which provided horizontal scrolling like terminator on Linux (see e.g. here).

What I wanted to provide in this blog is another solution. Usually the issue is with VARCHAR2-output. So I asked myself, why not formatting all VARCHAR2 columns of a table to their average length. I.e. I created a script colpp_table.sql (colpp, because my initial objective was to provide a column-width per page like in sqlcl) which takes the statistic avg_col_len from ALL_TAB_COLUMNS. To run the script I have to provide 2 parameters: The owner and table-name I use in my query later on:

cbleile@orcl@orcl> !more colpp_table.sql
set termout off heading off lines 200 pages 999 trimspool on feed off timing off verify off
spool /tmp/&1._&2..sql
select 'column '||column_name||' format a'||to_char(decode(nvl(avg_col_len,data_length),0,1,nvl(avg_col_len,data_length))) 
from all_tab_columns 
where owner=upper('&1.') 
and table_name=upper('&2.') 
and data_type in ('VARCHAR2','NVARCHAR2');
spool off
@/tmp/&1._&2..sql
set termout on heading on feed on timing on verify on

cbleile@orcl@orcl> @colpp_table CBLEILE T1

I.e. a temporary script /tmp/<owner>_<table_name>.sql gets written with format-commands for all VARCHAR2 (and NVARCHAR2) columns of the table. That temporary script is automatically executed:

cbleile@orcl@orcl> !cat /tmp/CBLEILE_T1.sql

column OWNER format a5
column OBJECT_NAME format a36
column SUBOBJECT_NAME format a2
column OBJECT_TYPE format a10
column TIMESTAMP format a20
column STATUS format a7
column TEMPORARY format a2
column GENERATED format a2
column SECONDARY format a2
column EDITION_NAME format a1
column SHARING format a14
column EDITIONABLE format a2
column ORACLE_MAINTAINED format a2
column APPLICATION format a2
column DEFAULT_COLLATION format a4
column DUPLICATED format a2
column SHARDED format a2

Now the formatting looks much better without having to format each column manually:

cbleile@orcl@orcl> @colpp_table CBLEILE T1
cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1 where rownum < 4;

OWNER OR OBJECT_NAME
----- -- ------------------------------------
SYS   Y  TS$
SYS   Y  ICOL$
SYS   Y  C_FILE#_BLOCK#

3 rows selected.

But what happens when selecting from a view? The column avg_col_len in all_tab_columns is NULL for views:

cbleile@orcl@orcl> create view t1v as select * from t1;
cbleile@orcl@orcl> column column_name format a21
cbleile@orcl@orcl> select column_name, avg_col_len from all_tab_columns where owner=user and table_name='T1V';

COLUMN_NAME           AVG_COL_LEN
--------------------- -----------
EDITIONABLE
ORACLE_MAINTAINED
APPLICATION
DEFAULT_COLLATION
DUPLICATED
SHARDED
CREATED_APPID
CREATED_VSNID
MODIFIED_APPID
MODIFIED_VSNID
OWNER
OBJECT_NAME
SUBOBJECT_NAME
OBJECT_ID
DATA_OBJECT_ID
OBJECT_TYPE
CREATED
LAST_DDL_TIME
TIMESTAMP
STATUS
TEMPORARY
GENERATED
SECONDARY
NAMESPACE
EDITION_NAME
SHARING

26 rows selected.

My idea was to do the following: Why not taking the “bytes”-computation per column from the optimizer divided by the number of rows returned by the view? I.e.

cbleile@orcl@orcl> explain plan for select owner from t1v;

Explained.

cbleile@orcl@orcl> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
---------------------------------
Plan hash value: 3617692013

--------------------------------------------------------------------------
| Id  | Operation         | Name | Rows  | Bytes | Cost (%CPU)| Time	 |
--------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |	     | 67944 |   331K|   372   (1)| 00:00:01 |
|   1 |  TABLE ACCESS FULL| T1   | 67944 |   331K|   372   (1)| 00:00:01 |
--------------------------------------------------------------------------

8 rows selected.

Now I just calculate the Bytes/Rows. As I just select 1 column I do get approximately the avg_col_len with that:

cbleile@orcl@orcl> select 331000/67944 from dual;

331000/67944
------------
  4.87165901

That value is close to the avg_col_len statistic of the underlying table:

cbleile@orcl@orcl> select avg_col_len from user_tab_columns where table_name='T1' and column_name='OWNER';

AVG_COL_LEN
-----------
          5

So the remaining question was just where to get the bytes and cardinality computation from? It’s in the plan_table:

cbleile@orcl@orcl> select bytes, cardinality, ceil(bytes/cardinality) avg_col_len from plan_table where id=0;

     BYTES CARDINALITY AVG_COL_LEN
---------- ----------- -----------
    339720       67944           5

With that information I had everything to create a script colpp_explain.sql:

cbleile@orcl@orcl> !more colpp_explain.sql
set termout off heading off lines 200 pages 999 trimspool on feed off timing off verify off
set serveroutput on size unlimited
spool /tmp/&1._&2..sql
declare
   avg_col_len number;
begin
   for i in (select column_name from all_tab_columns where owner=upper('&1.') and table_name=upper('&2.') and data_type in ('VARCHAR2','NVARCHAR2')) loop
      delete from plan_table;
      execute immediate 'explain plan for select '||i.column_name||' from &1..&2.';
      select ceil(bytes/cardinality) into avg_col_len from plan_table where id=0;
      dbms_output.put_line('column '||i.column_name||' format a'||to_char(avg_col_len+1));
   end loop;
end;
/
spool off
@/tmp/&1._&2..sql
set termout on heading on feed on timing on verify on
set serveroutput off

I.e. I’m looping through all columns with type VARCHAR2 (or NVARCHAR2) of the view and do an

explain plan for
select <column> from <view>;

With that information I can compute the avg_col_len by dividing the bytes by the cardinality and add the column formatting command to a script, which I finally execute.

cbleile@orcl@orcl> @colpp_explain CBLEILE T1V
cbleile@orcl@orcl> !cat /tmp/CBLEILE_T1V.sql
column EDITIONABLE format a3
column ORACLE_MAINTAINED format a3
column APPLICATION format a3
column DEFAULT_COLLATION format a5
column DUPLICATED format a3
column SHARDED format a3
column OWNER format a6
column OBJECT_NAME format a37
column SUBOBJECT_NAME format a3
column OBJECT_TYPE format a11
column TIMESTAMP format a21
column STATUS format a8
column TEMPORARY format a3
column GENERATED format a3
column SECONDARY format a3
column EDITION_NAME format a67
column SHARING format a15

cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1v where rownum < 4;

OWNER  ORA OBJECT_NAME
------ --- -------------------------------------
SYS    Y   TS$
SYS    Y   ICOL$
SYS    Y   C_FILE#_BLOCK#

3 rows selected.

To use a single script for tables and views I created a simple wrapper-script around colpp_table.sql and colpp_explain.sql:

cbleile@orcl@orcl> !cat colpp.sql
set termout off heading off lines 200 pages 999 trimspool on feed off timing off verify off
define var=colpp_explain.sql
column objt new_value var
select decode(object_type,'TABLE','colpp_table.sql','colpp_explain.sql') objt from all_objects where owner=upper('&1.') and object_name=upper('&2.');
@@&var. &1. &2.
set termout on heading on feed on timing on verify on

I.e. if the parameter is a table-name I do call colpp_table.sql. Otherwise I call colpp_explain.sql.

Finally it looks as follows:

For the table:

cbleile@orcl@orcl> @colpp CBLEILE T1
cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1v where rownum < 4;

OWNER OR OBJECT_NAME
----- -- ------------------------------------
SYS   Y  TS$
SYS   Y  ICOL$
SYS   Y  C_FILE#_BLOCK#

For the view:

cbleile@orcl@orcl> @colpp CBLEILE T1V
cbleile@orcl@orcl> select owner, oracle_maintained, object_name from t1v where rownum < 4;

OWNER  ORA OBJECT_NAME
------ --- -------------------------------------
SYS    Y   TS$
SYS    Y   ICOL$
SYS    Y   C_FILE#_BLOCK#

I.e. with a call to colpp.sql I can format all VARCHAR-columns of a table or a view. It’s of course not perfect, but easy, quick and better than the default-settings. You may even extend the scripts to also provide the heading per column or specify a sql_id or a script as parameters to colpp.sql.