{"id":1649,"date":"2020-12-20T23:57:12","date_gmt":"2020-12-20T21:57:12","guid":{"rendered":"https:\/\/blog.sqlora.com\/en\/?p=1649"},"modified":"2024-01-07T13:13:18","modified_gmt":"2024-01-07T11:13:18","slug":"yet-another-print_table-as-a-sql-macro","status":"publish","type":"post","link":"https:\/\/blog.sqlora.com\/en\/yet-another-print_table-as-a-sql-macro\/","title":{"rendered":"Yet another PRINT_TABLE &#8211; as a SQL Macro!"},"content":{"rendered":"\n<p>A few weeks ago Jonathan Lewis has published a <a rel=\"noreferrer noopener\" aria-label=\"note about Tom Kyte's print_table (opens in a new tab)\" href=\"https:\/\/jonathanlewis.wordpress.com\/2020\/11\/26\/print_table\/\" target=\"_blank\">note about Tom Kyte&#8217;s print_table<\/a> &#8211;  a small PL\/SQL procedure to output each row in a table as a list of <em>(column_name , value)<\/em>.  And since this note has gained some comments with other implementations, here is my contribution. Guess how? Of course with a SQL macro. <\/p>\n\n\n\n<!--more-->\n\n\n\n<p>So what we need is instead of this output:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; select * from scott.emp where empno = 7369;\n\n  EMPNO ENAME   JOB      MGR HIREDATE             SAL  COMM  DEPTNO\n------- ------- ------ ----- ------------------- ---- ----- -------\n   7369 SMITH   CLERK   7902 17.12.1980 00:00:00  800            20\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 1: a row in a table <\/em><\/strong><\/p>\n\n\n\n<p> a way to produce the output like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nEMPNO                         : 7369\nENAME                         : SMITH\nJOB                           : CLERK\nMGR                           : 7902\nHIREDATE                      : 17-dec-1980 00:00:00\nSAL                           : 800\nCOMM                          :\nDEPTNO                        : 20\n-----------------\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 2: the desired output: each column in a separate row<\/em><\/strong><\/p>\n\n\n\n<p>Why not just use UNPIVOT clause? Well the obvious problem with this approach is that you have to list the columns in the <em><strong>unpivot-in-clause<\/strong><\/em>:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"630\" height=\"217\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/12\/unpivot.png\" alt=\"\" class=\"wp-image-1655\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/12\/unpivot.png 630w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/12\/unpivot-300x103.png 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/12\/unpivot-624x215.png 624w\" sizes=\"auto, (max-width: 630px) 100vw, 630px\" \/><\/figure>\n\n\n\n<p>That wouldn&#8217;t be a reusable dynamic approach but require considerable effort each time you want to produce such output. <\/p>\n\n\n\n<p>Yet another problem is that all columns in the  <em><strong>unpivot-in-clause<\/strong><\/em>  should be of the same data type. Whereas the first SQL with just two numeric columns works fine, trying to include the column <strong><code>ENAME<\/code><\/strong> in the second one ends up with ORA-01790:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; set echo on\nSQL&gt; select col_name, col_value\n  2  from  scott.emp\n  3        unpivot include nulls \n  4        (col_value for col_name in (empno, mgr)  );\n\nCOL_N  COL_VALUE\n----- ----------\nEMPNO       7369\nMGR         7902\nEMPNO       7499\nMGR         7698\n... \n\nSQL&gt; select col_name, col_value\n  2  from  scott.emp\n  3        unpivot include nulls \n  4        (col_value for col_name in (empno, ename, mgr)  );\n\nError at Command Line : 53 Column : 42\nError report -\nSQL Error: ORA-01790: expression must have same datatype \n            as corresponding expression\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 3: using UNPIVOT leads to ORA-01790<\/em><\/strong><\/p>\n\n\n\n<p>The workaround could be to cast all data types to string before doing unpivot like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; select col_name, col_value\n  2  from  (select to_char(empno) as empno\n  3         ,      ename\n  4         ,      to_char(mgr) as mgr\n  5         from   scott.emp)\n  6        unpivot include nulls \n  7        (col_value for col_name in (empno, ename, mgr)  );\n\nCOL_N COL_VALUE                               \n----- -----------------\nEMPNO 7369                                    \nENAME SMITH                                   \nMGR   7902                                    \nEMPNO 7499                                    \nENAME ALLEN                                   \nMGR   7698   \n...\n<\/pre><\/div>\n\n\n<p><em><strong>Listing 4: the reference query <\/strong><\/em><\/p>\n\n\n\n<p>Now having this query as a reference we can try to implement a reusable function which will act on every table or subquery, whose structure we don&#8217;t need to know, which we can simple &#8220;call and forget&#8221;. Nice use case for SQL macros!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">SQL Macro<\/h2>\n\n\n\n<p>First of all, it would be a table macro, because we plan to pass data into the function and to be able to select a transformed output from it.<\/p>\n\n\n\n<p>Second, we&#8217;ll be passing a table parameter to it, making our macro a &#8220;polymorphic view&#8221;. By doing so we get the metadata about the structure of the used table via the data type <code><strong>DBMS_TF.TABLE_T<\/strong><\/code>: what columns do we have, what the data types are, and so on. Using this metadata we are able to consistently convert all data types to string and to dynamically build the <strong><code>UNPIVOT<\/code><\/strong> clause (lines 12-21). All we then have to do, is to put together a valid SQL statement and return it. Look how we are just referencing the table parameter <strong><em>T<\/em><\/strong> in the query (line 26). It will be substituted for us after we exit this function, returning the result string, with whatever was used as a parameter in the calling query: this can be a table name, a view,  a subquery or maybe a table function. In fact, we don&#8217;t need to know what it actually was. For the macro function it&#8217;s just enough to know about the parameter <strong><em>T<\/em><\/strong> providing all the needed metadata.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; CREATE OR REPLACE FUNCTION print_table(t DBMS_TF.TABLE_T ) \n  2  RETURN VARCHAR2 SQL_MACRO\n  3  AS\n  4   v_cols clob ;\n  5   v_unpivot  clob ;\n  6   v_str   varchar2(200);\n  7   v_delimiter varchar2(9):= &#039;,&#039;;\n  8   v_name dbms_id;\n  9   v_sql clob;\n 10  BEGIN\n 11    FOR I IN 1..t.column.count LOOP\n 12      v_name := t.column(i).description.name;\n 13      IF t.column(i).description.type = dbms_tf.type_varchar2 THEN  \n 14        v_str := v_name;\n 15      ELSIF t.column(i).description.type = dbms_tf.type_number THEN\n 16        v_str := &#039;to_char(&#039;||v_name||&#039;) as &#039;||v_name;\n 17      ELSIF t.column(i).description.type = dbms_tf.type_date THEN\n 18        v_str := &#039;to_char(&#039;||v_name||&#039;,&#039;&#039;YYYY-MM-DD HH24:MI:SS&#039;&#039;) as &#039;||v_name;\n 19      END IF;\n 20      v_cols := v_cols || v_delimiter || v_str;\n 21      v_unpivot := v_unpivot || v_delimiter || v_name;\n 22    END LOOP;\n 23    v_cols := LTRIM(v_cols,&#039;,&#039;);\n 24    v_unpivot := LTRIM(v_unpivot,&#039;,&#039;);\n 25    v_sql := &#039;SELECT col_name, nvl(col_value,&#039;&#039;(NULL)&#039;&#039;) as col_value &#039;||\n 26           &#039;FROM   (SELECT &#039;|| v_cols ||&#039; from t )&#039;||\n 27           &#039; UNPIVOT include nulls (col_value for col_name \n                                    in (&#039;||v_unpivot||&#039;)  )&#039;;\n 28    RETURN v_sql;         \n 29  END;\n 30  \/\n\nFunction PRINT_TABLE compiled\n\nSQL&gt; with data as (select * from scott.emp where empno = 7369)\n  2  select * from print_table(data);\n\nCOL_NAME COL_VALUE                               \n-------- ----------------------------------------\nEMPNO    7369                                    \nENAME    SMITH                                   \nJOB      CLERK                                   \nMGR      7902                                    \nHIREDATE 1980-12-17 00:00:00                     \nSAL      800                                     \nCOMM     (NULL)                                  \nDEPTNO   20                                      \n\n8 rows selected. \n\nSQL&gt; select * from print_table(scott.dept);\n\nCOL_NA COL_VALUE                               \n------ ----------------------------------------\nDEPTNO 10                                      \nDNAME  ACCOUNTING                              \nLOC    NEW YORK                                \nDEPTNO 20                                      \nDNAME  RESEARCH                                \nLOC    DALLAS                                  \nDEPTNO 30                                      \nDNAME  SALES                                   \nLOC    CHICAGO                                 \nDEPTNO 40                                      \nDNAME  OPERATIONS                              \nLOC    BOSTON                                  \n\n12 rows selected. \n<\/pre><\/div>\n\n\n<p><strong><em>Listing 5: the implementation as a SQL macro works<\/em><\/strong><\/p>\n\n\n\n<p>Works fine! Well, almost fine. You don&#8217;t have to change anything to use the function with another table (DEPT). That&#8217;s why polymorphic. But this second query apparently needs some kind of separator between the data of different rows. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Adding a row separator<\/h2>\n\n\n\n<p>Since we don&#8217;t use PL\/SQL to print things like the original print_table does but just SELECTing, our separator should just be the part of the query result. What we can do is to introduce another artificial column \/ value pair as if they were in every queried table and they will act as a separator. BTW, I never thought, I can use &#8220;&#8212;&#8212;&#8211;&#8221; as a column name&#8230; <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nCREATE OR REPLACE FUNCTION print_table(t DBMS_TF.TABLE_T ) \nRETURN VARCHAR2 SQL_MACRO\nAS\n v_cols clob ;\n v_unpivot  clob ;\n v_str   varchar2(200);\n v_delimiter varchar2(9):= &#039;,&#039;;\n v_name dbms_id;\n v_sql clob;\nBEGIN\n  FOR I IN 1..t.column.count LOOP\n    v_name := t.column(i).description.name;\n    IF t.column(i).description.type = dbms_tf.type_varchar2 THEN  \n      v_str := v_name;\n    ELSIF t.column(i).description.type = dbms_tf.type_number THEN\n      v_str := &#039;to_char(&#039;||v_name||&#039;) as &#039;||v_name;\n    ELSIF t.column(i).description.type = dbms_tf.type_date THEN\n      v_str := &#039;to_char(&#039;||v_name||&#039;,&#039;&#039;YYYY-MM-DD HH24:MI:SS&#039;&#039;) as &#039;||v_name;\n    END IF;\n    v_cols := v_cols || v_delimiter || v_str;\n    v_unpivot := v_unpivot || v_delimiter || v_name;\n  END LOOP;\n  v_cols := LTRIM(v_cols,&#039;,&#039;);\n  v_unpivot := LTRIM(v_unpivot,&#039;,&#039;);\n  v_sql := &#039;SELECT col_name, nvl(col_value,&#039;&#039;(NULL)&#039;&#039;) as col_value &#039;||\n         &#039;FROM   (SELECT &#039;|| v_cols ||&#039;, \n                  &#039;&#039;-------------&#039;&#039; as &quot;--------&quot; from t )&#039;||\n         &#039; UNPIVOT include nulls (col_value for col_name \n              in (&#039;||v_unpivot||&#039;, &quot;--------&quot;)  )&#039;;\n  return v_sql;         \nEND;\n\/\n\nSQL&gt; select * from print_table(scott.dept);\n\nCOL_NAME COL_VALUE                               \n-------- ----------------------------------------\nDEPTNO   10                                      \nDNAME    ACCOUNTING                              \nLOC      NEW YORK                                \n-------- -------------                           \nDEPTNO   20                                      \nDNAME    RESEARCH                                \nLOC      DALLAS                                  \n-------- -------------                           \nDEPTNO   30                                      \nDNAME    SALES                                   \nLOC      CHICAGO                                 \n-------- -------------                           \nDEPTNO   40                                      \nDNAME    OPERATIONS                              \nLOC      BOSTON                                  \n-------- -------------                           \n\n16 rows selected. \n<\/pre><\/div>\n\n\n<p><strong><em>Listing 6: PRINT_TABLE with row separators<\/em><\/strong><\/p>\n\n\n<script async src=\"\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2925154690547867\" crossorigin=\"anonymous\"><\/script><ins class=\"adsbygoogle\" style=\"display:block; text-align:center;\" data-ad-client=\"ca-pub-2925154690547867\" \ndata-ad-slot=\"2727403138\" \ndata-ad-layout=\"in-article\"\ndata-ad-format=\"fluid\"><\/ins>\n<script> \n(adsbygoogle = window.adsbygoogle || []).push({}); \n<\/script>\n\n\n\n<p>Now, you may ask, why we had used a subquery on line 35 of the Listing 5? Why not just select from the function and put the WHERE clause in the same query? If you are experienced with PTF&#8217;s (polymorphic table functions) introduced in 18c, you may think it should work.  Finally, if <em><strong>print_table<\/strong><\/em> were a PTF, the behavior was advertised as an important performance feature: filtering, partition pruning, projection, etc. all can be done before passing the data into the PTF. But it wouldn&#8217;t work with a SQL macro:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; select * from print_table(scott.emp) where empno = 7369;\n\nSQL Error: ORA-00904: &quot;EMPNO&quot;: invalid identifier\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 7: cannot filter in the same query<\/em><\/strong><\/p>\n\n\n\n<p>And it&#8217;s a major difference you should clearly understand. With SQL macros you don&#8217;t have any data flow into a macro. It&#8217;s all during parsing, so actually where we call  <code><strong>print_table(scott.emp)<\/strong><\/code>, the <code><strong>UNPIVOT<\/strong><\/code> query returned from the function will remain. And this query has only two columns: COL_NAME and COL_VALUE, so you cannot filter using EMPNO. Instead, you could filter using COL_NAME:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; select * from print_table(scott.emp) where col_name = &#039;EMPNO&#039;;\n\nCOL_NAME COL_VALUE                               \n-------- ----------------------------------------\nEMPNO    7369                                    \nEMPNO    7499                                    \nEMPNO    7521               \n...\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 8: you have only access to the columns remaining after the transformation made by SQL macro<\/em><\/strong><\/p>\n\n\n\n<p>Seeing this, it could be another reasonable requirement: how can we output only a subset of columns?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Printing a subset of columns  <\/h2>\n\n\n\n<p>It&#8217;s pretty straightforward. All we need is an optional parameter of type <code><strong>DBMS_TF.COLUMNS_T<\/strong><\/code> and an IF condition to only consider the columns in list if a column list was not empty (line 14):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; highlight: [14]; title: ; notranslate\" title=\"\">\nCREATE OR REPLACE FUNCTION print_table(t DBMS_TF.TABLE_T\n                                     , cols DBMS_TF.COLUMNS_T DEFAULT NULL ) \nRETURN VARCHAR2 SQL_MACRO\nAS\n v_cols clob ;\n v_unpivot  clob ;\n v_str   varchar2(200);\n v_delimiter varchar2(9):= &#039;,&#039;;\n v_name dbms_id;\n v_sql clob;\nBEGIN\n  FOR I IN 1..t.column.count LOOP\n    v_name := t.column(i).description.name;\n    IF v_name MEMBER OF cols OR cols IS NULL THEN\n        IF t.column(i).description.type = dbms_tf.type_varchar2 THEN  \n          v_str := v_name;\n        ELSIF t.column(i).description.type = dbms_tf.type_number THEN\n          v_str := &#039;to_char(&#039;||v_name||&#039;) as &#039;||v_name;\n        ELSIF t.column(i).description.type = dbms_tf.type_date THEN\n          v_str := &#039;to_char(&#039;||v_name||&#039;,&#039;&#039;YYYY-MM-DD HH24:MI:SS&#039;&#039;) as &#039;||v_name;\n        END IF;\n        v_cols := v_cols || v_delimiter || v_str;\n        v_unpivot := v_unpivot || v_delimiter || v_name;\n    END IF;    \n  END LOOP;\n  v_cols := LTRIM(v_cols,&#039;,&#039;);\n  v_unpivot := LTRIM(v_unpivot,&#039;,&#039;);\n  v_sql := &#039;SELECT col_name, nvl(col_value,&#039;&#039;(NULL)&#039;&#039;) as col_value &#039;||\n         &#039;FROM   (SELECT &#039;|| v_cols ||&#039;, \n                         &#039;&#039;-------------&#039;&#039; as &quot;--------&quot; from t )&#039;||\n         &#039; UNPIVOT include nulls (col_value for col_name in \n                      (&#039;||v_unpivot||&#039;, &quot;--------&quot;)  )&#039;;\n  return v_sql;         \nEND;\n\/\n\nSQL&gt; select * from print_table(scott.emp, columns(empno, ename));\n\nCOL_NAME COL_VALUE                               \n-------- -----------------\nEMPNO    7369                                    \nENAME    SMITH                                   \n-------- -------------                           \nEMPNO    7499                                    \nENAME    ALLEN                                   \n-------- ------------- \n...\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 9: PRINT_TABLE with optionally column subsets<\/em><\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary <\/h2>\n\n\n\n<p>Really easy and straightforward implementation,  but useful and reusable. One of those use cases where we benefit from the provided metadata and can make the code dependent on the column&#8217;s data type. <\/p>\n\n\n<div class=\"crp-list-container\"><h3 class=\"crp-list-title\">Related Posts<\/h3><ul class=\"crp-list\"><li class=\"crp-list-item crp-list-item-image-none\"><div class=\"crp-list-item-title\"><a href=\"https:\/\/blog.sqlora.com\/en\/building-hash-keys-using-sql-macros-in-oracle-20c\/\" target=\"_blank\">Building Hash Keys using SQL Macros in Oracle 20c<\/a><\/div><\/li><li class=\"crp-list-item crp-list-item-image-none\"><div class=\"crp-list-item-title\"><a href=\"https:\/\/blog.sqlora.com\/en\/comparing-rows-using-a-sql-macro\/\" target=\"_blank\">Comparing Rows using a SQL Macro<\/a><\/div><\/li><li class=\"crp-list-item crp-list-item-image-none\"><div class=\"crp-list-item-title\"><a href=\"https:\/\/blog.sqlora.com\/en\/oracle-20c-sql-macros\/\" target=\"_blank\">Oracle 20c: SQL Macros<\/a><\/div><\/li><li class=\"crp-list-item crp-list-item-image-none\"><div class=\"crp-list-item-title\"><a href=\"https:\/\/blog.sqlora.com\/en\/sql-macros-in-oracle-19c\/\" target=\"_blank\">SQL macros in Oracle 19c<\/a><\/div><\/li><\/ul><\/div>\n\n<script async src=\"\/\/pagead2.googlesyndication.com\/pagead\/js\/adsbygoogle.js?client=ca-pub-2925154690547867\" crossorigin=\"anonymous\"><\/script><ins class=\"adsbygoogle\" style=\"display:block; text-align:center;\" data-ad-client=\"ca-pub-2925154690547867\" \ndata-ad-slot=\"2727403138\" \ndata-ad-layout=\"in-article\"\ndata-ad-format=\"fluid\"><\/ins>\n<script> \n(adsbygoogle = window.adsbygoogle || []).push({}); \n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>A few weeks ago Jonathan Lewis has published a note about Tom Kyte&#8217;s print_table &#8211; a small PL\/SQL procedure to output each row in a table as a list of (column_name , value). And since this note has gained some comments with other implementations, here is my contribution. Guess how? Of course with a SQL [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1669,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,3,100],"tags":[113,103,57],"class_list":["post-1649","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oracle","category-sql","category-sql-macros","tag-key-value","tag-sql-macros","tag-unpivot"],"_links":{"self":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1649","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/comments?post=1649"}],"version-history":[{"count":23,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1649\/revisions"}],"predecessor-version":[{"id":1676,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1649\/revisions\/1676"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media\/1669"}],"wp:attachment":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media?parent=1649"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/categories?post=1649"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/tags?post=1649"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}