{"id":483,"date":"2010-09-15T18:43:28","date_gmt":"2010-09-15T18:43:28","guid":{"rendered":"http:\/\/bar-solutions.com\/weblog\/?p=483"},"modified":"2010-09-15T18:43:28","modified_gmt":"2010-09-15T17:43:28","slug":"","status":"publish","type":"post","link":"https:\/\/blog.bar-solutions.com\/?p=483","title":{"rendered":"Hashing rows\u2026"},"content":{"rendered":"<p>During my work at the current customer site I was asked to create files with the changes to the data in the database. Normally you would use triggers on the tables to create logs of the records changed. Or use a Flashback Data Archive, which is only available starting Oracle 11G and I am working with Oracle 10G. The trouble in this situation is that we want to see if something changed on the data of a materialized view. This materialized view is fully refreshed overnight.<\/p>\n<p>First idea I had was to check for the RowID. I figured that would change when the row was changed. Problem is that the RowID either always changes, whether or not the data changed, or didn\u2019t change, even when the data changed. This proved not to be a reliable way to check for changes in the data.<\/p>\n<\/p>\n<p><!--more--><\/p>\n<p>Of course I could make a copy of the MV before it gets refreshed, but that would mean duplicating a lot of data. And of course we would have to check every column for changes. This would be an easy task if the tables don\u2019t have too many columns and if they are simple columns (i.e. varchar2s, numbers and dates).<\/p>\n<p>How about making some sort of <a href=\"http:\/\/en.wikipedia.org\/wiki\/Cryptographic_hash_function\">hash<\/a> value for the entire row and store that value. This involved a lot of thinking and some investigating. Chances that 2 rows will hash to the same value are really small. According to <a href=\"http:\/\/stackoverflow.com\/questions\/2444321\/how-are-hash-functions-like-md5-unique\">Stack Overflow<\/a>:<\/p>\n<blockquote>\n<p>(there are <strong>2<sup>128<\/sup><\/strong> possible hashes total so the chance of two random strings producing the same hash is theoretically close to 1 in 10<sup>38<\/sup>).<\/p>\n<\/blockquote>\n<p>That means hashing is not flawless, but it is good (enough).<\/p>\n<p>I want to make it dynamic, I don\u2019t want to create similar code for every table (or materialized view in this case) I need to check. These can be tens, maybe hundreds or even thousands. How can I make this a generic solution. I can of course put all the fields of a record in one big string and hash that string. Let\u2019s check some documentation. First of all, I need to find all the columns in the table. Luckily <a href=\"http:\/\/www.oracle.com\/\">Oracle<\/a> provides us with a <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B10501_01\/server.920\/a96524\/c05dicti.htm\">data dictionary<\/a> in which we can found out just about everything we need to know about our objects. Column information is stored in the <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B19306_01\/server.102\/b14237\/statviews_2094.htm\">ALL_TAB_COLUMNS<\/a> view (ALL for all objects we have access to. We can also use <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B19306_01\/server.102\/b14237\/statviews_4462.htm\">USER_TAB_COLUMNS<\/a> but that shows us only the columns in table in our own schema. There is also DBA_TAB_COLUMNS, which is pretty much the same as ALL_TAB_COLUMNS but it needs the DBA Role and it shows us every column in every table in every schema).<\/p>\n<p>I need the names and types of columns in a certain table (possibly owned by a different user) so I need to retrieve the columns COLUMN_NAME and DATA_TYPE based on TABLE_NAME and OWNER. The SQL statement I need to execute is something like this:<\/p>\n<p><font size=\"2\" face=\"Courier\">SELECT atc.column_name<br \/>\n    <br \/>&#160;&#160;&#160;&#160; , atc.data_type<\/p>\n<p>&#160; FROM all_tab_columns atc<\/p>\n<p> WHERE atc.table_name = &lt;table_name&gt;<\/p>\n<p>&#160;&#160; and atc.OWNER = &lt;owner&gt;<\/font><\/p>\n<p>Since I am using PL\/SQL it is easy to return everything I need in one roundtrip to the SQL Engine using <a href=\"http:\/\/download.oracle.com\/docs\/cd\/E11882_01\/appdev.112\/e17126\/tuning.htm#LNPLS01205\">Bulk Operations<\/a>. The means I want to <a href=\"http:\/\/download.oracle.com\/docs\/cd\/E11882_01\/appdev.112\/e17126\/tuning.htm#i49139\">BULK COLLECT<\/a> whatever is returned. When I retrieve a single value I can use a scalar variable to select into. But now I will retrieve probably more than one row, so I need a collection to fetch into. Actually I need a collection of records so I create a record and a collection in my package:<\/p>\n<p><font size=\"2\" face=\"Courier\">TYPE column_rt IS RECORD(<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; column_name user_tab_columns.column_name%TYPE<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">, data_type user_tab_columns.data_type%TYPE);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">TYPE column_aat IS TABLE OF column_rt INDEX BY BINARY_INTEGER;<\/font><\/p>\n<p>Using this collection type and my SQL statement I create a function like this:<\/p>\n<p><font size=\"2\" face=\"Courier\">FUNCTION get_column_names(table_name_in IN VARCHAR2<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">,owner_in IN VARCHAR2 DEFAULT NULL) RETURN column_aat IS<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; l_returnvalue column_aat;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">SELECT atc.column_name<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">, atc.data_type BULK COLLECT<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">INTO l_returnvalue<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">FROM all_tab_columns atc<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160; WHERE atc.table_name = table_name_in<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160; AND (atc.owner = owner_in OR owner_in IS NULL);<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">RETURN l_returnvalue;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">END get_column_names;<\/font><\/p>\n<p>Now that I know what columns are in the table I can construct an SQL statement to retrieve all columns concatenated in one column. Something like this:<\/p>\n<p><font size=\"2\" face=\"Courier\">SELECT &lt;concatenated_columns&gt; &lt;alias&gt;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">FROM &lt;user&gt;.&lt;table&gt; &lt;table_alias&gt;<\/font><\/p>\n<p>I also need to find a way to link the result back to the originating table. The only way (I can think of) I can do this (without relying on Primary Keys which may or may not be defined) is using the ROWID. So my SQL statement will become something like this:<\/p>\n<p><font size=\"2\" face=\"Courier\">SELECT &lt;concatenated_columns&gt; &lt;alias&gt;, rowid<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">FROM &lt;user&gt;.&lt;table&gt; &lt;table_alias&gt;<\/font><\/p>\n<p>Using <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B10500_01\/appdev.920\/a96624\/11_dynam.htm\">Native Dynamic SQL<\/a> I can construct the SQL statement at runtime, hence using the data dictionary to define what I really select.<\/p>\n<p>Oracle does a lot of implicit conversion on which I could rely, but I think it is better (and faster) to do explicit conversion. That way I have more control over the actual statement being generated. Also, my statement will fail if there is no implicit conversion available (and it did, since I have a couple of <a href=\"http:\/\/www.oracle.com\/technetwork\/database\/options\/spatial\/index.html\">Oracle Spatial<\/a> columns in my test Materialized Views).<\/p>\n<p>So, to build the select part of the statement which is dynamic I created another function:<\/p>\n<p><font size=\"2\" face=\"Courier\">FUNCTION create_select(columns_in IN column_aat<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; ,table_name_in IN VARCHAR2<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">,owner_in IN VARCHAR2) RETURN VARCHAR2 IS<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; l_prefix VARCHAR2(30);<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">l_suffix VARCHAR2(30);<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">l_skip BOOLEAN;<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">l_returnvalue maxvarchar;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">l_returnvalue := &#8221;;<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">FOR indx IN nvl(columns_in.first, 0) .. nvl(columns_in.last, -1) LOOP<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_skip := FALSE;<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">CASE columns_in(indx).data_type<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">WHEN c_varchar THEN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_prefix := c_prefix_varchar;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_suffix := c_suffix_varchar;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">WHEN c_number THEN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_prefix := c_prefix_number;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_suffix := c_suffix_number;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END;<br \/>\n    <br \/><\/font>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <font size=\"2\" face=\"Courier\">WHEN c_date THEN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_prefix := c_prefix_date;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_suffix := c_suffix_date;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">ELSE<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_prefix := c_prefix_empty;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_suffix := c_suffix_empty;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_skip := TRUE;<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">logging.add(9, &#8216;data_type&#8217;, columns_in(indx).data_type);<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END;<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END CASE;<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">IF NOT (l_skip) THEN<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">l_returnvalue := l_returnvalue || &#8216;|| &#8216; || l_prefix || columns_in(indx).column_name || l_suffix;<br \/>\n    <br \/>&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">END IF;<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">END LOOP;<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">RETURN l_returnvalue;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">END create_select;<\/font><\/p>\n<p>As you can see in the code, I am currently only hashing the scalar fields in the table. This makes it easier for me to test. If there are special columns (like the Spatial datatype) it gets skipped for now, but I do log that this datatype is currently not processed (In the currently working version I do have some code to do hashing on the SDO_GEOMETRY column type). This function can \u2018easily\u2019 be expanded to reflect new, different types of columns that I might encounter. I could even create support for <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B19306_01\/server.102\/b14195\/sqlqr06.htm\">User Defined dataTypes<\/a> that I may store in a table.<\/p>\n<p>I now have a SQL statement that will return two columns. The concatenated values from a row and the rowid. The rowid must be returned as is. This will be the way of connecting the originating table to the value returned by this bit of PL\/SQL. The concatenated value of the entire row should be hashed so it returns a value of predefined size (Varchar2(32)). <\/p>\n<p>To hash a value I use the hash function in the <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B19306_01\/appdev.102\/b14258\/d_crypto.htm\">dbms_crypto<\/a> package. Since I don\u2019t want to write the code needed here in my statements (hide the logic behind a layer of code (<a href=\"http:\/\/www.toadworld.com\/sf\">Steven Feuerstein<\/a>)) I created a simple function for it:<\/p>\n<p><font size=\"2\" face=\"Courier\">FUNCTION hashvalue(value_in IN maxvarchar) RETURN VARCHAR2 IS<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">RETURN dbms_crypto.hash(src =&gt; utl_i18n.string_to_raw(value_in, &#8216;AL32UTF8&#8217;)<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,typ =&gt; dbms_crypto.hash_md5);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">END hashvalue;<\/font><\/p>\n<p>(maxvarchar is a <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B19306_01\/appdev.102\/b14261\/datatypes.htm#i42580\">subtype<\/a> defined so I don\u2019t need to remember the maximum size of a varchar in PL\/SQL<\/p>\n<p><font size=\"2\" face=\"Courier\">SUBTYPE maxvarchar IS VARCHAR2(32767);<br \/>\n    <br \/><\/font>)<\/p>\n<p>Now I need to create a <a href=\"http:\/\/www.oracle.com\/technology\/sample_code\/tech\/pl_sql\/htdocs\/x\/Table_Functions_Cursor_Expressions\/start.htm\">table function<\/a> (a function that can be called from SQL using the TABLE() operator) that gets me the hashed values for all the rows.<\/p>\n<p>To be able to access more than one field in the resultset I need this function to return a schema level defined collection type. In this case a <a href=\"http:\/\/download.oracle.com\/docs\/cd\/B10501_01\/appdev.920\/a96624\/05_colls.htm\">Nested Table Type<\/a>.<\/p>\n<p><font size=\"2\" face=\"Courier\">CREATE OR REPLACE TYPE hash_rt AS OBJECT<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">( <\/font><font size=\"2\" face=\"Courier\">hrt_rowid VARCHAR2(18)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">, <\/font><font size=\"2\" face=\"Courier\">hrt_hashvalue VARCHAR2(32)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">create or replace type hash_ntt as table of hash_rt<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p>The code needs to know what table it should run against and who the owner of this object is. So the function specification becomes:<\/p>\n<p><font size=\"2\" face=\"Courier\">FUNCTION hashrows(table_name_in IN VARCHAR2<br \/>\n    <br \/>&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">,owner_in IN VARCHAR2 DEFAULT USER) RETURN hash_ntt<\/font><\/p>\n<p>By adding the keyword PIPELINED to this specification I can utilize possible parallel execution of any SQL I write against this function.<\/p>\n<p><font size=\"2\" face=\"Courier\">FUNCTION hashrows( table_name_in IN VARCHAR2<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; , owner_in IN VARCHAR2 DEFAULT USER) RETURN hash_ntt<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">PIPELINED IS<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; TYPE valrow_r IS RECORD(<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">vr_value maxvarchar<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; , vr_rowid maxvarchar);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">TYPE valrow_aat IS TABLE OF valrow_r INDEX BY BINARY_INTEGER;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_valrow valrow_aat;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_columns column_aat;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_cv cv_type;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_sql maxvarchar;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">BEGIN<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; get the columns for this table.<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_columns := get_column_names(upper(table_name_in), upper(owner_in));<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; build the select statement<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">l_sql := &#8216;SELECT &#8216; || ltrim(create_select(l_columns, table_name_in, owner_in), &#8216;|&#8217;) ||<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#8216; fullrow, rowid from &#8216; || owner_in || &#8216;.&#8217; || table_name_in || &#8216; &#8216; || c_table_alias;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; open the cursor using this select statement<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">OPEN l_cv FOR l_sql;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; bulk collect the results<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">FETCH l_cv BULK COLLECT<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">INTO l_valrow;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; check to see if there is anything to process.<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; <\/font><font size=\"2\" face=\"Courier\">IF l_valrow.count &gt; 0 THEN<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; loop though the result<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; <\/font><font size=\"2\" face=\"Courier\">FOR indx IN l_valrow.first .. l_valrow.last LOOP<br \/>\n    <br \/><\/font><i><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160; &#8212; pipe the result out of the function as soon as it is known<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160; PIPE ROW(hash_rt(l_valrow(indx).vr_rowid, hashvalue(l_valrow(indx).vr_value)));<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; END LOOP;&#160;&#160;&#160; <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; END IF;<br \/>\n    <br \/><\/font><i><font size=\"2\" face=\"Courier\">&#160; &#8212; close the cursor<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; CLOSE l_cv;<br \/>\n    <br \/><\/font><i><font size=\"2\" face=\"Courier\">&#160; &#8212; return control<br \/>\n      <br \/><\/font><\/i><font size=\"2\" face=\"Courier\">&#160; RETURN;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">EXCEPTION<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; WHEN OTHERS THEN<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; logging.add(9, &#8216;Error&#8217;, SQLERRM);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; logging.add(9, &#8216;Error Stack&#8217;, dbms_utility.format_error_stack);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160; logging.add(9, &#8216;SQL Statement&#8217;, l_sql);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">END hashrows;<\/font><\/p>\n<p>By choosing the pipelined option I am obliged to PIPE each ROW when it\u2019s done processing out of the function. At the end of the function I have nothing to RETURN but control, so therefore I have a simple return at the end of the function (no value is being returned).<\/p>\n<p>Since my SQL returns multiple rows and I want to BULK COLLECT them I need a collection. In this case (again) a collection of records, since I am fetching more than one column per record. I could have created a package level type, but there is no need for this type outside this function, so I decided to declare it here.<\/p>\n<p>I PIPE out each row which is of the rowtype I defined. It is kind of strange that the function says it will return a collection but actually it is returning single rows. But if you think about it, it does return more than one row, just one at a time.<\/p>\n<p>Since the create_select function returns the column names with two | at the beginning of the string (saves a lot of if-then processing) I need to trim these bars from the beginning in building the statement:<\/p>\n<p><font size=\"2\" face=\"Courier\">ltrim(create_select(l_columns, table_name_in, owner_in)<\/font><\/p>\n<p>Now, lets see what this function returns. Unfortunately this function cannot be called from PL\/SQL. Running this testscript:<\/p>\n<p><font size=\"2\" face=\"Courier\">declare<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">result hash_ntt;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">begin<br \/>\n    <br \/>&#160; <\/font><i><font size=\"2\" face=\"Courier\">&#8212; Call the function<br \/>\n      <br \/>&#160; <\/font><\/i><font size=\"2\" face=\"Courier\">result := hashing.hashrows(table_name_in =&gt; &#8216;MV_HASH&#8217;);<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">end;<\/font><\/p>\n<p>Results in an Oracle error:<\/p>\n<p><font size=\"2\" face=\"Courier\">ORA-06550: line 19, column 12:<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">PLS-00653: aggregate\/table functions are not allowed in PL\/SQL scope<\/font><\/p>\n<p>So I need to call it from SQL using a statement like this:<\/p>\n<p><font size=\"2\" face=\"Courier\">select * from table(hashing.hashrows(&#8216;MV_HASH&#8217;))<\/font><\/p>\n<p>This results in:<\/p>\n<p><font size=\"2\" face=\"Courier\">HRT_ROWID HRT_HASHVALUE<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#8212;&#8212;&#8212;&#8212;&#8212;&#8212; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAA AC5B43F5442765E5C27C96B069EC9B6A<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAB 3FEF7C64CBEF9C04D6FD767787343E67<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAC 4F22E776622A31C15AF0D3CF0E7DCA50<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAD 49FDD474072F7EB55A2073E3B2DA5475<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAE 8CEA084275D4010464E0965EADD1E910<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">AAFXh7AAEAAAA9EAAF 234E9FA7FD39430372C60FFEAD77FD6F<br \/>\n    <br \/><\/font>\u2026\u2026\u2026<\/p>\n<p>Now that looks cool. Not something you would want to read though.<\/p>\n<p>Let\u2019s do some testing. First I create two tables. 1 for the \u2018real\u2019 data and one to store the hashed values in (along with the rowed).<\/p>\n<p><font size=\"2\" face=\"Courier\">create table mv_hash (<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; id number<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">, name varchar2(30)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">create table ht_hash (<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; ht_rowid varchar2(18)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">, ht_hashvalue varchar2(32)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p>Then I insert some rows into the first table (which is called mv_ because I will be using this on a Materialized View in the real world)<\/p>\n<p>insert into mv_hash (id, name) (select t.object_id, t.OBJECT_NAME from user_objects t)<br \/>\n  <br \/>\/<\/p>\n<p>603 rows inserted<\/p>\n<p>commit<\/p>\n<p>\/<\/p>\n<p>Now, insert the hash values of the mv_table into the ht_ (HashTable) table.<\/p>\n<p><font size=\"2\" face=\"Courier\">insert into ht_hash (ht_rowid, ht_hashvalue) (select hrt_rowid, hrt_hashvalue from table(hashing.hashrows(&#8216;MV_HASH&#8217;)))<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">603 rows inserted<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">commit<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p>Then it\u2019s time to update a row in the mv_ table<\/p>\n<p><font size=\"2\" face=\"Courier\">update mv_hash<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160; set name = &#8216;Patrick&#8217;<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">where rownum = 1<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">commit<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p><font size=\"2\" face=\"Courier\">1 row updated<\/font><\/p>\n<p>Let\u2019s find out which row was updated<\/p>\n<p><font size=\"2\" face=\"Courier\">select *<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; from mv_hash m<br \/>\n    <br \/>&#160; <\/font><font size=\"2\" face=\"Courier\">left outer join ht_hash h <\/font><font size=\"2\" face=\"Courier\">on (m.rowid = h.ht_rowid)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\"> where h.ht_rowid is null<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p>It seems the rowed hasn\u2019t changed by the update. Let\u2019s see if we can find it using the entire table, the hashtable and the table function too.<\/p>\n<p><font size=\"2\" face=\"Courier\">select *<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; from mv_hash m<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160; join (table(hashing.hashrows(&#8216;MV_HASH&#8217;)) hm<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160;&#160; left outer join ht_hash h on (hm.hrt_hashvalue = h.ht_hashvalue)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">&#160;&#160;&#160;&#160;&#160;&#160; ) on (hm.hrt_rowid = m.rowid)<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\"> where h.ht_hashvalue is null<br \/>\n    <br \/><\/font><font size=\"2\" face=\"Courier\">\/<\/font><\/p>\n<p>It is the way to get to the changed row. Actually I am doing an ANTI-JOIN (outer-join with one of the joined columns being null, i.e. not available)<\/p>\n<p>This check works even when the entire table is truncated and filled again since we don\u2019t check the rowid, but the hashvalue for the row. If the hashvalue hasn\u2019t changed, it won\u2019t show up.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>During my work at the current customer site I was asked to create files with the changes to the data in the database. Normally you would use triggers on the tables to create logs of the records changed. Or use a Flashback Data Archive, which is only available starting Oracle 11G and I am working [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,5,9],"tags":[],"class_list":["post-483","post","type-post","status-publish","format-standard","hentry","category-oracle","category-plsql","category-sql"],"_links":{"self":[{"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/posts\/483","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=483"}],"version-history":[{"count":1,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/posts\/483\/revisions"}],"predecessor-version":[{"id":484,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=\/wp\/v2\/posts\/483\/revisions\/484"}],"wp:attachment":[{"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=483"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=483"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.bar-solutions.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=483"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}