{"id":1316,"date":"2020-03-28T19:37:36","date_gmt":"2020-03-28T17:37:36","guid":{"rendered":"https:\/\/blog.sqlora.com\/en\/?p=1316"},"modified":"2020-03-30T08:26:00","modified_gmt":"2020-03-30T06:26:00","slug":"temporal-joins-with-sql-macros-in-oracle-20c","status":"publish","type":"post","link":"https:\/\/blog.sqlora.com\/en\/temporal-joins-with-sql-macros-in-oracle-20c\/","title":{"rendered":"Temporal Joins with SQL Macros in Oracle 20c"},"content":{"rendered":"\n<p>In a <a rel=\"noreferrer noopener\" aria-label=\"previous post (opens in a new tab)\" href=\"https:\/\/blog.sqlora.com\/en\/parameterized-views-in-oracle-no-problem-with-sql-macros\/\" target=\"_blank\">previous post<\/a> on SQL macros in Oracle Database 20c we saw how SQL macros can be used to create a kind of &#8220;parameterized&#8221; views to establish a simplified access tier to temporal data. In this post I&#8217;d like to explore more possibilities to hide the complexity of SQL statements behind a functional syntax provided by using SQL macros. As an example we&#8217;ll stay with a temporal data introduced in the <a href=\"https:\/\/blog.sqlora.com\/en\/parameterized-views-in-oracle-no-problem-with-sql-macros\/\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\"previous post (opens in a new tab)\">previous post<\/a> and explore how we can do a temporal join of this versioned data.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h4 class=\"wp-block-heading\">What is a temporal join? <\/h4>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"alignleft\"><img loading=\"lazy\" decoding=\"async\" width=\"388\" height=\"476\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/dwhp.png\" alt=\"\" class=\"wp-image-1334\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/dwhp.png 388w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/dwhp-245x300.png 245w\" sizes=\"auto, (max-width: 388px) 100vw, 388px\" \/><\/figure><\/div>\n\n\n\n<p>About a month ago I had the opportunity to join a Data Warehouse Design Patterns Workshop by <a rel=\"noreferrer noopener\" aria-label=\"Roelant Vos (opens in a new tab)\" href=\"https:\/\/twitter.com\/RoelantVos\" target=\"_blank\">Roelant Vos<\/a>. Yes, despite of <a href=\"https:\/\/twitter.com\/hashtag\/COVID19\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\"#COVID19 (opens in a new tab)\">#COVID19<\/a>, at that time it was not yet quite unthinkable to fly to Zurich to attend a workshop in person \ud83d\ude41 <\/p>\n\n\n\n<p>Being one of the many interesting topics of this three-day training, the question of joining temporal data arises e.g. when calculating the dimensions from several versioned tables.<\/p>\n\n\n\n<p>Unlike &#8220;point in time&#8221; queries, temporal joins produce all possible combinations of validity intervals in the source tables. Although the way you can do it was not new for me, it is quite complex and it led me to the idea of testing the use of SQL macros for that task. <\/p>\n\n\n\n<p>My colleague <a rel=\"noreferrer noopener\" aria-label=\"Philipp Salvisberg (opens in a new tab)\" href=\"https:\/\/twitter.com\/phsalvisberg\" target=\"_blank\">Philipp Salvisberg<\/a>, whose <a rel=\"noreferrer noopener\" aria-label=\"data model and data (opens in a new tab)\" href=\"https:\/\/www.salvis.com\/blog\/wp-content\/uploads\/2012\/12\/00_setup_data.sql_.txt\" target=\"_blank\">data model and data<\/a> I&#8217;m using in this example has blogged about <a rel=\"noreferrer noopener\" aria-label=\"this kind of queries (opens in a new tab)\" href=\"https:\/\/www.salvis.com\/blog\/2012\/12\/24\/joining-temporal-intervals\/\" target=\"_blank\">this kind of queries<\/a> and I have tested the use of <a rel=\"noreferrer noopener\" aria-label=\"row pattern matching clause to merge the resulting  time interval (opens in a new tab)\" href=\"https:\/\/blog.sqlora.com\/en\/merging-temporal-intervals-using-match_recognize\/\" target=\"_blank\">row pattern matching clause to merge the resulting  time interval<\/a>s a time ago. I will use Philipp&#8217;s picture to show what we are doing:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"746\" height=\"408\" src=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/TemporalJoinOverview1.png\" alt=\"\" class=\"wp-image-1329\" srcset=\"https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/TemporalJoinOverview1.png 746w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/TemporalJoinOverview1-300x164.png 300w, https:\/\/blog.sqlora.com\/en\/wp-content\/uploads\/sites\/2\/2020\/03\/TemporalJoinOverview1-624x341.png 624w\" sizes=\"auto, (max-width: 746px) 100vw, 746px\" \/><figcaption><strong><em>Figure 1:<\/em><\/strong> Temporal Join<\/figcaption><\/figure>\n\n\n\n<p>We produce all the time intervals containing different data from joined tables. The exact way of doing this may differ depending on whether you are using open or closed time intervals, can you have gaps, would you like to merge adjacent intervals with the same data and so on. But the exact shape of the query is not important for our purpose. I&#8217;d only like to demonstrate how we can implement some kind of functional syntax  to support complex types of queries. Let&#8217;s say, we prefer following solution for the problem:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nWITH  joined AS (\n      -- gap-aware temporal join\n      -- produces result_cols to calculate new_group in the subsequent query\n      SELECT e.empno,\n             g.valid_from,\n             LEAST(\n                e.valid_to,\n                d.valid_to,\n                LEAD(g.valid_from - 1, 1, e.valid_to) OVER(\n                   PARTITION BY e.empno ORDER BY g.valid_from\n                )\n             ) AS valid_to,\n             e.ename,\n             e.mgr,\n             e.hiredate,\n             e.sal,\n             e.comm,\n             e.deptno,\n             d.dname            \n        FROM empv e\n       INNER JOIN (SELECT valid_from FROM empv\n                   UNION\n                   SELECT valid_from FROM deptv\n                   UNION\n                   SELECT valid_to + 1 FROM empv\n                    WHERE valid_to != DATE '9999-12-31'\n                   UNION\n                   SELECT valid_to + 1 FROM deptv\n                    WHERE valid_to != DATE '9999-12-31') g\n          ON g.valid_from BETWEEN e.valid_from AND e.valid_to\n       INNER JOIN deptv d\n          ON d.deptno = e.deptno AND g.valid_from BETWEEN d.valid_from AND d.valid_to\n          )\nSELECT empno\n,      valid_from\n,      valid_to\n,      ename\n,      mgr\n,      hiredate\n,      sal\n,      comm\n,      deptno\n,      dname\nFROM joined\n      MATCH_RECOGNIZE (\n        PARTITION BY empno\n                   , ename\n                   , mgr\n                   , hiredate\n                   , sal\n                   , comm\n                   , deptno\n                   , dname            \n        ORDER BY valid_from\n        MEASURES FIRST(valid_from) valid_from, LAST(valid_to) valid_to\n        ONE ROW PER MATCH\n        PATTERN ( strt nxt* )\n        DEFINE nxt as valid_from = prev(valid_to) + 1\n        )\n\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 1: The shape of the query doing temporal join<\/em><\/strong><\/p>\n\n\n\n<p>Well, quite lengthy and complex, isn&#8217;t it? Note, that I reduced the join from the Figure 1 to join only two tables. What I&#8217;d like to have, is a SQL macro which I could call instead of this whole complex query. Just like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt;\nSQL&gt;\nSQL&gt;  SELECT * FROM temporal_join ( &lt;parameter&gt; );\nSQL&gt;\nSQL&gt;\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 2: the desired functional syntax<\/em><\/strong><\/p>\n\n\n\n<p>Would it be possible? If you&#8217;ve read my <a rel=\"noreferrer noopener\" aria-label=\"previous posts about SQL macros (opens in a new tab)\" href=\"https:\/\/blog.sqlora.com\/en\/tag\/sql-macros\/\" target=\"_blank\">previous posts about SQL macros<\/a>, you&#8217;ll be confident it is. Why not? Let&#8217;s start. <\/p>\n\n\n\n<p>What parameter do we need to dynamically build a statement like the one from Listing 1? We are joining two tables (let&#8217;s start with two for now) so we should know these tables and have two table parameters (DBMS_TF.Table_t). Many things are convention based in this simple example: I suppose to know the the validity column names, that we are using closed time intervals, that we are using a special date 31-DEC-9999 and so on. But there are also things I cannot know and must pass them as parameters. I have to know the business key of the first table (EMPNO) because it is used in PARTITION clause of the analytical function LEAD (line 9 in Listing 1). We define the parameter <strong><code>bk_cols<\/code><\/strong> of type <strong><code>DBMS_TF.COLUMNS_T<\/code><\/strong> for this purpose and can pass just one or even a list of columns in case of a multi-column key.  <\/p>\n\n\n\n<p>And I have to know how to join those two tables. For simplification I suppose we will always have an equality conditions and I guess it will be true in 99% of cases. Then we just have to know the column names of the columns used as join criteria. I decided to use just one column list (<strong><code>join_cols DBMS_TF.COLUMNS_T<\/code><\/strong>)  but in a special way: a pair of columns to join  followed by another pair if any and so on. For example, if the join condition looks like <code><strong>t1.a=t2.b AND t1.c=t2.d<\/strong><\/code> then you&#8217;d have  <code><strong>COLUMNS(a,b,c,d). <\/strong><\/code><\/p>\n\n\n\n<p>The rest of the implementation is rather boring putting together the desired SQL. I put the function in the package TIME_MACHINE introduced in the previous post about <a href=\"https:\/\/blog.sqlora.com\/en\/parameterized-views-in-oracle-no-problem-with-sql-macros\/\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\"point in time queries in a kind of &quot;parameterized&quot; views:   (opens in a new tab)\">point in time queries in a kind of &#8220;parameterized&#8221; views:  <\/a><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nCREATE OR REPLACE PACKAGE time_machine IS\n...\n...\nFUNCTION temporal_join(t1 DBMS_TF.Table_t\n                     , t2 DBMS_TF.Table_t\n                     , bk_cols DBMS_TF.COLUMNS_T\n                     , join_cols DBMS_TF.COLUMNS_T) \nRETURN VARCHAR2 SQL_MACRO(TABLE);\n\nEND time_machine;\n\/\nCREATE OR REPLACE PACKAGE BODY time_machine IS\n...\n...\nFUNCTION temporal_join(t1 DBMS_TF.Table_t\n                     , t2 DBMS_TF.Table_t\n                     , bk_cols DBMS_TF.COLUMNS_T\n                     , join_cols DBMS_TF.COLUMNS_T) \nRETURN VARCHAR2 SQL_MACRO(TABLE)\nAS\n -- convention based \n v_dates varchar2(1000);\n v_select varchar2(4000);\n v_buskey varchar2(1000);\n v_join_keys varchar2(1000);\n v_name DBMS_ID;\n v_column_list varchar2(1000);\n v_mr varchar2(2000);\nBEGIN\n   v_dates := q'&#x5B;INNER JOIN (SELECT t1.valid_from FROM t1\n                   UNION\n                   SELECT t2.valid_from FROM t2\n                   UNION\n                   SELECT t1.valid_to + 1 FROM t1\n                    WHERE t1.valid_to != DATE '9999-12-31'\n                   UNION\n                   SELECT t2.valid_to + 1 FROM t2\n                    WHERE t2.valid_to != DATE '9999-12-31') g\n          ON g.valid_from BETWEEN t1.valid_from AND t1.valid_to]';\n    FOR I IN 1..t1.column.count LOOP\n      v_name := t1.column(i).description.name;\n      IF v_name NOT IN ('\"VALID_FROM\"', '\"VALID_TO\"') THEN\n        V_Select := v_select ||', t1.'||v_name||' as '||\n                   replace(t1.table_name,'\"')||'_'||replace(v_name,'\"');\n        v_column_list := v_column_list ||', '||\n                   replace(t1.table_name,'\"')||'_'||replace(v_name,'\"');\n      END IF; \n      IF v_name MEMBER OF bk_cols THEN\n        v_buskey := v_buskey || ', t1.' || v_name;\n      END IF;\n    END LOOP;\n    FOR I IN 1..t2.column.count LOOP\n      v_name := t2.column(i).description.name;\n      IF v_name NOT IN ('\"VALID_FROM\"', '\"VALID_TO\"') THEN\n        V_Select := v_select ||', t2.'||v_name||' as '||\n                  replace(t2.table_name,'\"')||'_'||replace(v_name,'\"');\n        v_column_list := v_column_list ||', '||\n                  replace(t2.table_name,'\"')||'_'||replace(v_name,'\"');\n      END IF; \n    END LOOP;\n    v_column_list := ltrim(v_column_list , ',');\n    v_buskey := ltrim(v_buskey , ',');\n    v_select := 'SELECT '||\n           ltrim(v_select , ',')||\n             q'&#x5B;, g.valid_from,\n             LEAST(\n                t1.valid_to,\n                t2.valid_to,\n                LEAD(g.valid_from - 1, 1, t1.valid_to) OVER(\n                   PARTITION BY ]'||v_buskey||q'&#x5B; ORDER BY g.valid_from\n                )\n             ) AS valid_to\n        FROM t1 ]'\n        || v_dates;\n    FOR i in 1..join_cols.count LOOP\n       IF MOD(i,2) = 0 THEN    \n          v_join_keys := 'AND t1.'||join_cols(i-1)|| '= t2.'||join_cols(i);\n       END IF;\n    END LOOP; \n    v_join_keys := 'ON '||ltrim(v_join_keys , 'AND');\n    v_select := v_select ||' INNER JOIN t2 '||v_join_keys||\n                ' AND g.valid_from BETWEEN t2.valid_from AND t2.valid_to  ';\n    v_mr := ' MATCH_RECOGNIZE ( PARTITION BY '||v_column_list||q'&#x5B;\n        ORDER BY valid_from\n        MEASURES FIRST(valid_from) valid_from, LAST(valid_to) valid_to\n        ONE ROW PER MATCH\n        PATTERN ( strt nxt* )\n        DEFINE nxt as valid_from = prev(valid_to) + 1\n        )]'; \n   v_select := 'SELECT '||v_column_list||\n               ', r_valid_from valid_from, r_valid_to valid_to '||\n               'FROM ('||v_select ||') '|| v_mr;            \n   return v_select ;    \nEND;\n\nEND time_machine;\n\/\n\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 3: SQL macro function implementing temporal join<\/em><\/strong><\/p>\n\n\n\n<p>Note, how we are using the names of the table parameters T1 and T2 as table names across the whole strings. Inside the strings, without any concatenation. Remember, they will be replaced with real table names just at the time when the result string is returned out of the function. <\/p>\n\n\n\n<p>To be able to return a unique set of columns we are prefixing all column names with a table name. If you don&#8217;t like it, you can do something else, but you should have some convention at place. Otherwise you&#8217;ll get &#8220;duplicate column name&#8221; error. One of the solutions could also be passing column list parameters for each table to define what columns should be in the result set and thus being able to control the uniqueness yourself . <\/p>\n\n\n\n<p>Well, now try to call the function:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; SELECT * \nFROM   time_machine.temporal_join (t1 =&gt; empv\n                     ,t2 =&gt; deptv\n                     ,bk_cols =&gt; columns(empno)\n                     ,join_cols =&gt; columns( deptno, deptno))\nWHERE  empv_empno = 7788\n\nEMPV_EMPVID EMPV_EMPNO EMPV_ENAME     EMPV_JOBNO   EMPV_MGR EMPV_HIREDATE   EMPV_SAL  EMPV_COMM EMPV_DEPTNO DEPTV_DEPTVID DEPTV_DEPTNO DEPTV_DNAME    DEPTV_LOC     VALID_FROM VALID_TO \n----------- ---------- -------------- ---------- ---------- ------------- ---------- ---------- ----------- ------------- ------------ -------------- ------------- ---------- ---------\n          8       7788 SCOTT                   5       7566 19-APR-87           3000                     20             2           20 RESEARCH       DALLAS        19-APR-87  31-DEC-89\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20             2           20 RESEARCH       DALLAS        01-JAN-90  28-FEB-90\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20             6           20 Research       DALLAS        01-MAR-90  31-MAR-90\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20            10           20 Research       Dallas        01-APR-90  31-MAR-91\n         36       7788 Scott                   5       7566 19-APR-87           3300                     20            10           20 Research       Dallas        01-APR-91  31-DEC-99\n\n5 rows selected.\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 4: Calling a SQL macro function <\/em><\/strong><\/p>\n\n\n\n<p>Looks good! It did what we expected. We can hide really complex implementation behind just one function call. And remember that in fact no PL\/SQL functions are involved at runtime, slowing down your query. The SQL macro function is called only once at parse time and generates the query text which will be incorporated into the invoking query.  What actually runs, is a SQL statement much like the one in Listing 1. You can verify it by calling <code><strong>DBMS_UTILITY.EXPAND_SQL_TEXT<\/strong><\/code> as I did in a <a rel=\"noreferrer noopener\" aria-label=\"previous post (opens in a new tab)\" href=\"https:\/\/blog.sqlora.com\/en\/parameterized-views-in-oracle-no-problem-with-sql-macros\/\" target=\"_blank\">previous post<\/a>. <\/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<h4 class=\"wp-block-heading\">Joining more than two tables<\/h4>\n\n\n\n<p>Now what about joining three tables? Can we just nest the function calls? Unfortunately not. Neither as inline view, nor as conventional view, nor  as common table expression (CTE) aka named subquery aka WITH clause subquery. That seems to be an intended restriction since we get a specific error message: <code><strong>ORA-64630: unsupported use of SQL macro: use of SQL macro inside WITH clause is not supported,<\/strong> <\/code>even with conventional views.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\n&gt;&gt; WITH empdept AS(\nSELECT * \nFROM   time_machine.temporal_join (t1 =&gt; empv\n                     ,t2 =&gt; deptv\n                     ,bk_cols =&gt; columns(empno)\n                     ,join_cols =&gt; columns( deptno, deptno))\n) \nSELECT * \nFROM   time_machine.temporal_join (t1 =&gt; empdept\n                     ,t2 =&gt; jobv\n                     ,bk_cols =&gt; columns(empno)\n                     ,join_cols =&gt; columns( jobno, jobno))\nWHERE  empv_empno = 7788\n  *\nError at line 0\nORA-64630: unsupported use of SQL macro: use of SQL macro inside WITH clause is not supported\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 5: Using of SQL macro inside WITH clause or views is not supported <\/em><\/strong><\/p>\n\n\n\n<p>So I&#8217;d suggest just to implement the second, third, etc. function which accept three, four and more tables. I know, this doesn&#8217;t look like an elegant solution. But I don&#8217;t see the better one at the moment. In fact you put all these overloaded functions in a package. Another thing I would do, is to provide some generic helper functions to return building blocks for the query in every situation. The task of the SQL macro function would just be to correctly pass their parameters calling these helper functions. If you start with two macros for joining two and three tables at once and one day you&#8217;ll see that you need to join four tables, then adding a new overloaded SQL macro function would be a matter of minutes. I&#8217;ll try to test it soon and will provide the implementation here. For now, I just implemented two very similar overloaded functions in a package to show that it works. <\/p>\n\n\n\n<pre class=\"brush: sql; collapse: true; light: false; title: ; toolbar: true; notranslate\" title=\"\"> \n\nCREATE OR REPLACE PACKAGE time_machine IS\n\nFUNCTION vw (p_tab DBMS_TF.Table_t,  p_eff_date date) \nRETURN VARCHAR2 SQL_MACRO;\n\nFUNCTION vw (p_tab DBMS_TF.Table_t,  p_eff_date date, p_exp_date date) \nRETURN VARCHAR2 SQL_MACRO;\n\nFUNCTION temporal_join(t1 DBMS_TF.Table_t\n                     , t2 DBMS_TF.Table_t\n                     , bk_cols DBMS_TF.COLUMNS_T\n                     , join_cols DBMS_TF.COLUMNS_T) \nRETURN VARCHAR2 SQL_MACRO(TABLE);\n\nFUNCTION temporal_join(t1 DBMS_TF.Table_t\n                     , t2 DBMS_TF.Table_t\n                     , t3 DBMS_TF.Table_t\n                     , bk_cols DBMS_TF.COLUMNS_T\n                     , join_cols1 DBMS_TF.COLUMNS_T\n                     , join_cols2 DBMS_TF.COLUMNS_T) \nRETURN VARCHAR2 SQL_MACRO(TABLE);\n\nEND time_machine;\n\/\n\n\nCREATE OR REPLACE PACKAGE BODY time_machine IS\n\nFUNCTION vw (p_tab DBMS_TF.Table_t,  p_eff_date date) \nRETURN VARCHAR2 SQL_MACRO\nIS\nv_query varchar2(500);\nBEGIN\n  v_query := q'!SELECT * \n                FROM   p_tab \n                WHERE  p_eff_date between valid_from  AND valid_to!';\n  RETURN v_query;\nEND;\n\nFUNCTION vw (p_tab DBMS_TF.Table_t,  p_eff_date date, p_exp_date date) \nRETURN VARCHAR2 SQL_MACRO IS\nv_query varchar2(500);\nBEGIN\n  v_query := q'!SELECT * \n                FROM   p_tab \n                WHERE  valid_from &amp;amp;amp;lt;= p_exp_date \n                AND    valid_to &amp;amp;amp;gt;= p_eff_date!';\n  RETURN v_query;\nEND;\n  \nFUNCTION temporal_join(t1 DBMS_TF.Table_t\n                     , t2 DBMS_TF.Table_t\n                     , bk_cols DBMS_TF.COLUMNS_T\n                     , join_cols DBMS_TF.COLUMNS_T) \nRETURN VARCHAR2 SQL_MACRO(TABLE)\nAS\n -- convention based \n v_dates varchar2(1000);\n v_select varchar2(4000);\n v_buskey varchar2(1000);\n v_join_keys varchar2(1000);\n v_name DBMS_ID;\n v_column_list varchar2(1000);\n v_mr varchar2(2000);\nBEGIN\n   v_dates := q'&#x5B;INNER JOIN (SELECT t1.valid_from FROM t1\n                   UNION\n                   SELECT t2.valid_from FROM t2\n                   UNION\n                   SELECT t1.valid_to + 1 FROM t1\n                    WHERE t1.valid_to != DATE '9999-12-31'\n                   UNION\n                   SELECT t2.valid_to + 1 FROM t2\n                    WHERE t2.valid_to != DATE '9999-12-31') g\n          ON g.valid_from BETWEEN t1.valid_from AND t1.valid_to]';\n    FOR I IN 1..t1.column.count LOOP\n      v_name := t1.column(i).description.name;\n      IF v_name NOT IN ('&quot;VALID_FROM&quot;', '&quot;VALID_TO&quot;') THEN\n        V_Select := v_select ||', t1.'||v_name||' as '||replace(t1.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n        v_column_list := v_column_list ||', '||replace(t1.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n      END IF; \n      IF v_name MEMBER OF bk_cols THEN\n        v_buskey := v_buskey || ', t1.' || v_name;\n      END IF;\n    END LOOP;\n    FOR I IN 1..t2.column.count LOOP\n      v_name := t2.column(i).description.name;\n      IF v_name NOT IN ('&quot;VALID_FROM&quot;', '&quot;VALID_TO&quot;') THEN\n        V_Select := v_select ||', t2.'||v_name||' as '||replace(t2.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n        v_column_list := v_column_list ||', '||replace(t2.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n      END IF; \n    END LOOP;\n    v_column_list := ltrim(v_column_list , ',');\n    v_buskey := ltrim(v_buskey , ',');\n    v_select := 'SELECT '||\n           ltrim(v_select , ',')||\n             q'&#x5B;, g.valid_from,\n             LEAST(\n                t1.valid_to,\n                t2.valid_to,\n                LEAD(g.valid_from - 1, 1, t1.valid_to) OVER(\n                   PARTITION BY ]'||v_buskey||q'&#x5B; ORDER BY g.valid_from\n                )\n             ) AS valid_to\n        FROM t1 ]'\n        || v_dates;\n    FOR i in 1..join_cols.count LOOP\n       IF MOD(i,2) = 0 THEN    \n          v_join_keys := 'AND t1.'||join_cols(i-1)|| '= t2.'||join_cols(i);\n       END IF;\n    END LOOP; \n    v_join_keys := 'ON '||ltrim(v_join_keys , 'AND');\n    v_select := v_select ||' INNER JOIN t2 '||v_join_keys||\n                ' AND g.valid_from BETWEEN t2.valid_from AND t2.valid_to  ';\n    v_mr := ' MATCH_RECOGNIZE ( PARTITION BY '||v_column_list||q'&#x5B;\n        ORDER BY valid_from\n        MEASURES FIRST(valid_from) R_valid_from, LAST(valid_to) R_valid_to\n        ONE ROW PER MATCH\n        PATTERN ( strt nxt* )\n        DEFINE nxt as valid_from = prev(valid_to) + 1\n        )]'; \n   v_select := 'SELECT '||v_column_list||\n               ', r_valid_from valid_from, r_valid_to valid_to '||\n               'FROM ('||v_select ||') '|| v_mr;    \n             \n   return v_select ;    \nEND;\n  \n\n\nFUNCTION temporal_join(t1 DBMS_TF.Table_t, t2 DBMS_TF.Table_t, t3 DBMS_TF.Table_t, bk_cols DBMS_TF.COLUMNS_T, join_cols1 DBMS_TF.COLUMNS_T, join_cols2 DBMS_TF.COLUMNS_T)  \nRETURN VARCHAR2 SQL_MACRO(TABLE)\nAS\n v_h varchar2(300);\n -- convention based \n v_dates varchar2(1000);\n v_select varchar2(4000);\n v_buskey varchar2(1000);\n v_join_keys varchar2(1000);\n v_name DBMS_ID;\n v_column_list varchar2(1000);\n v_mr varchar2(2000); \nBEGIN\n   v_dates := q'&#x5B;INNER JOIN (SELECT t1.valid_from FROM t1\n                   UNION\n                   SELECT t2.valid_from FROM t2\n                   UNION\n                   SELECT t2.valid_from FROM t2\n                   UNION\n                   SELECT t1.valid_to + 1 FROM t1\n                    WHERE t1.valid_to != DATE '9999-12-31'\n                   UNION\n                   SELECT t2.valid_to + 1 FROM t2\n                    WHERE t2.valid_to != DATE '9999-12-31'\n                   UNION\n                   SELECT t2.valid_to + 1 FROM t2\n                    WHERE t2.valid_to != DATE '9999-12-31' ) g\n          ON g.valid_from BETWEEN t1.valid_from AND t1.valid_to]';\n    FOR I IN 1..t1.column.count LOOP\n      v_name := t1.column(i).description.name;\n      IF v_name NOT IN ('&quot;VALID_FROM&quot;', '&quot;VALID_TO&quot;') THEN\n        V_Select := v_select ||', t1.'||v_name||' as '||replace(t1.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n        v_column_list := v_column_list ||', '||replace(t1.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n      END IF; \n      IF v_name MEMBER OF bk_cols THEN\n        v_buskey := v_buskey || ', t1.' || v_name;\n      END IF;\n    END LOOP;\n    FOR I IN 1..t2.column.count LOOP\n      v_name := t2.column(i).description.name;\n      IF v_name NOT IN ('&quot;VALID_FROM&quot;', '&quot;VALID_TO&quot;') THEN\n        V_Select := v_select ||', t2.'||v_name||' as '||replace(t2.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n        v_column_list := v_column_list ||', '||replace(t2.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n      END IF; \n    END LOOP;\n    FOR I IN 1..t3.column.count LOOP\n      v_name := t3.column(i).description.name;\n      IF v_name NOT IN ('&quot;VALID_FROM&quot;', '&quot;VALID_TO&quot;') THEN\n        V_Select := v_select ||', t3.'||v_name||' as '||replace(t3.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n        v_column_list := v_column_list ||', '||replace(t3.table_name,'&quot;')||'_'||replace(v_name,'&quot;');\n      END IF; \n    END LOOP;\n    \n    v_column_list := ltrim(v_column_list , ',');\n    v_buskey := ltrim(v_buskey , ',');\n    v_select := 'SELECT '||\n           ltrim(v_select , ',')||\n             q'&#x5B;, g.valid_from,\n             LEAST(\n                t1.valid_to,\n                t2.valid_to,\n                LEAD(g.valid_from - 1, 1, t1.valid_to) OVER(\n                   PARTITION BY ]'||v_buskey||q'&#x5B; ORDER BY g.valid_from\n                )\n             ) AS valid_to\n        FROM t1 ]'\n        || v_dates;\n    FOR i in 1..join_cols1.count LOOP\n       IF MOD(i,2) = 0 THEN    \n          v_join_keys := 'AND t1.'||join_cols1(i-1)|| '= t2.'||join_cols1(i);\n       END IF;\n    END LOOP; \n    v_join_keys := 'ON '||ltrim(v_join_keys , 'AND');\n    v_select := v_select ||' INNER JOIN t2 '||v_join_keys||\n                ' AND g.valid_from BETWEEN t2.valid_from AND t2.valid_to  ';\n    FOR i in 1..join_cols2.count LOOP\n       IF MOD(i,2) = 0 THEN    \n          v_join_keys := 'AND t1.'||join_cols2(i-1)|| '= t3.'||join_cols2(i);\n       END IF;\n    END LOOP; \n    v_join_keys := 'ON '||ltrim(v_join_keys , 'AND');\n    v_select := v_select ||' INNER JOIN t3 '||v_join_keys||\n                ' AND g.valid_from BETWEEN t3.valid_from AND t3.valid_to  ';                \n    v_mr := ' MATCH_RECOGNIZE ( PARTITION BY '||v_column_list||q'&#x5B;\n        ORDER BY valid_from\n        MEASURES FIRST(valid_from) r_valid_from, LAST(valid_to) r_valid_to\n        ONE ROW PER MATCH\n        PATTERN ( strt nxt* )\n        DEFINE nxt as valid_from = prev(valid_to) + 1\n        )]'; \n    v_select := 'SELECT '||v_column_list||\n               ', r_valid_from valid_from, r_valid_to valid_to '||\n               'FROM ('||v_select ||') '|| v_mr;             \n   return v_select ;    \nEND;\n  \n\nEND time_machine;\n\/\n<\/pre>\n\n\n\n<p> <strong><em>Listing 6: Implementation of overloaded SQL macro functions<\/em><\/strong><br><\/p>\n\n\n\n<p>Because the code gets very long really fast, I&#8217;ll provide it initially collapsed. Listing 7 shows calling the function for three tables: <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; title: ; notranslate\" title=\"\">\nSQL&gt; SELECT * \nFROM   time_machine.temporal_join( t1 =&gt; empv\n                                 , t2 =&gt; deptv\n                                 , t3 =&gt; jobv\n                                 , bk_cols =&gt; columns(empno)\n                                 , join_cols1 =&gt; columns(deptno, deptno)\n                                 , join_cols2 =&gt; columns(jobno, jobno))\nwhere  empv_empno = 7788\n\nEMPV_EMPVID EMPV_EMPNO EMPV_ENAME     EMPV_JOBNO   EMPV_MGR EMPV_HIREDATE   EMPV_SAL  EMPV_COMM EMPV_DEPTNO DEPTV_DEPTVID DEPTV_DEPTNO DEPTV_DNAME    DEPTV_LOC     JOBV_JOBVID JOBV_JOBNO JOBV_JOB  VALID_FROM VALID_TO \n----------- ---------- -------------- ---------- ---------- ------------- ---------- ---------- ----------- ------------- ------------ -------------- ------------- ----------- ---------- --------- ---------- ---------\n          8       7788 SCOTT                   5       7566 19-APR-87           3000                     20             2           20 RESEARCH       DALLAS                  5          5 ANALYST   19-APR-87  31-DEC-89\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20             2           20 RESEARCH       DALLAS                  5          5 ANALYST   01-JAN-90  28-FEB-90\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20             6           20 Research       DALLAS                 10          5 Analyst   01-MAR-90  31-MAR-90\n         22       7788 Scott                   5       7566 19-APR-87           3000                     20            10           20 Research       Dallas                 10          5 Analyst   01-APR-90  31-MAR-91\n         36       7788 Scott                   5       7566 19-APR-87           3300                     20            10           20 Research       Dallas                 10          5 Analyst   01-APR-91  31-DEC-99\n\n5 rows selected.\n\n<\/pre><\/div>\n\n\n<p><strong><em>Listing 7: Calling a SQL macro function to join three tables <\/em><\/strong><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Summary <\/h4>\n\n\n\n<p>Besides the certain practical value of the solution for doing temporal joins with just one function call, I think it is a good example of how we can hide complex, recurring, boring and, most important, error-prone  SQL syntax inside a SQL macro function. If in the past you had certain biases against using the PL\/SQL functions in SQL, remember again, SQL macros are  different! They will only be executed at parse time. No extra PL\/SQL at runtime, no context switch!<\/p>\n\n\n\n<p>Last but not least, thank you,   <a rel=\"noreferrer noopener\" href=\"https:\/\/twitter.com\/RoelantVos\" target=\"_blank\">Roelant Vos<\/a>, for the insightful three days! And I think the implementation and most importantly the usage of even more of your patterns can be simplified using SQL macros in Oracle 20c. <\/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<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\/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-part-2-parameters-and-parsing\/\" target=\"_blank\">SQL Macros Part 2 &#8211; Passing of Parameters and Parsing<\/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\/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\/parameterized-views-in-oracle-no-problem-with-sql-macros\/\" target=\"_blank\">Parameterized Views in Oracle? No problem! With SQL macros!<\/a><\/div><\/li><\/ul><\/div>","protected":false},"excerpt":{"rendered":"<p>In a previous post on SQL macros in Oracle Database 20c we saw how SQL macros can be used to create a kind of &#8220;parameterized&#8221; views to establish a simplified access tier to temporal data. In this post I&#8217;d like to explore more possibilities to hide the complexity of SQL statements behind a functional syntax [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1317,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[99,71,2,3,100],"tags":[102,103,107],"class_list":["post-1316","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-20c","category-etl","category-oracle","category-sql","category-sql-macros","tag-20c","tag-sql-macros","tag-temporal-jon"],"_links":{"self":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1316","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=1316"}],"version-history":[{"count":84,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1316\/revisions"}],"predecessor-version":[{"id":1444,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/posts\/1316\/revisions\/1444"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media\/1317"}],"wp:attachment":[{"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/media?parent=1316"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/categories?post=1316"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.sqlora.com\/en\/wp-json\/wp\/v2\/tags?post=1316"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}