Varattrs tables in Oracle

A lot of programming languages allow to declare functions which accept arbitrary number of attributes and such functions are often referred as varargs functions. Even Oracle has such functions – for example, SQL function decode or PL/SQL function utl_lms.format_message. Unfortunately, Oracle does not allow to declare user defined varargs functions but this post is not about that.

From time to time Data Architects have to deal with the challenge to store entities with arbitrary attributes. If all possible entities are not known in advance during the schema design then it’s not possible to create a table with all the necessary attributes. Possible solutions in such cases are

  • EAV design + pivot
  • Storing entities in XML/JSON format or simply as varchar (latter would require implementing the logic for parsing)

Above solutions are not even close from performance perspective to the design when we can create a table with all needed attributes in advance. Nevertheless there is a solution with very similar performance.

What we can do is creating an object type for each entity which needs to be persisted in a table. The key advantage in terms of flexibility is that we do not need to create all the object types before creating a table – it is enough to have only base object in advance. Let me show how it works below.

 set feedback off
 
 create or replace type o force as object
 (
   key int
 )
 not final
 /
 create or replace type o_value under o
 (
   value int
 )
 /
 create or replace type o_person under o
 (
   name varchar2(30),
   dob  date
 )
 /
 
 create table t(id int generated as identity primary key, value o);
 
 insert into t(value) select o(1) from dual;
 insert into t(value) select o_value(2, 100) from dual;
 insert into t(value)
 select o_person(3, 'Ivan', date '2020-09-27') from dual;
 commit;
 
 select * from t;
 
 select t.value.key key
   from t t
  where t.value is of type (o);
 
 select t.value.key key,
        treat(t.value as o_value).value value
   from t t
  where t.value is of type (o_value);
 
 select t.value.key key,
        treat(t.value as o_person).name name,
        treat(t.value as o_person).dob dob
   from t t
  where t.value is of type (o_person);
 
 create or replace type o_dummy under o
 (
   str varchar2(30)
 )
 /
 
 insert into t(value) select o_dummy(1, 'Hello') from dual;
 
 select t.value.key key,
        treat(t.value as o_dummy).str str
   from t t
  where t.value is of type (o_dummy);
 
 commit;

Output

         ID VALUE
 ---------- ------
          1 O(1)
          2 O(2)
          3 O(3)
 
        KEY
 ----------
          1
          2
          3
 
        KEY      VALUE
 ---------- ----------
          2        100
 
        KEY NAME                           DOB
 ---------- ------------------------------ ---------
          3 Ivan                           27-SEP-20
 
        KEY STR
 ---------- ------------------------------
          1 Hello

As you may have noticed, type o_dummy was created when table already exists but we still can store values for a new object in the table because it is inherited from the base object. For brevity I named such tables varattrs tables because effectively this allows us to store entities with whatever attributes we want in a single table and access each attribute like it is a separate column.

So what about performance? Surprisingly it’s almost identical for varattrs table and normal relational table. For below test execution time on my laptop is 6.9 vs 7.0 seconds. Please note that there is no specific order without order by thus sample output may be different.

 set serveroutput on
 
 drop table t1;
 drop table t2;
 drop type o force;
 
 create table t1
 (
   n1     number,
   n2     number,
   n3     number,
   n4     number,
   n5     number,
   n6     number,
   n7     number,
   n8     number,
   n9     number,
   n10    number,
   n11    number,
   n12    number,
   n13    number,
   n14    number,
   n15    number,
   n16    number,
   n17    number,
   n18    number,
   n19    number,
   n20    number,
   n21    number,
   n22    number,
   n23    number,
   n24    number,
   n25    number,
   n26    number,
   n27    number,
   n28    number,
   n29    number,
   n30    number
 );
 
 create or replace type o force as object(key int) not final;
 /
 
 create or replace type o_30 under o
 (
   n1 number,
   n2 number,
   n3 number,
   n4 number,
   n5 number,
   n6 number,
   n7 number,
   n8 number,
   n9 number,
   n10 number,
   n11 number,
   n12 number,
   n13 number,
   n14 number,
   n15 number,
   n16 number,
   n17 number,
   n18 number,
   n19 number,
   n20 number,
   n21 number,
   n22 number,
   n23 number,
   n24 number,
   n25 number,
   n26 number,
   n27 number,
   n28 number,
   n29 number,
   n30 number
 )
 /
 
 create table t2(value o);
 insert into t1
   select level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level,
          level
   from dual
   connect by level <= 2e6;
 
 insert into t2(value)
   select o_30(level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level,
               level)
   from dual
   connect by level <= 2e6;
 
 commit;
 
 set timing on
 
 declare
   cursor c is
     select n1,
            n2,
            n3,
            n4,
            n5,
            n6,
            n7,
            n8,
            n9,
            n10,
            n11,
            n12,
            n13,
            n14,
            n15,
            n16,
            n17,
            n18,
            n19,
            n20,
            n21,
            n22,
            n23,
            n24,
            n25,
            n26,
            n27,
            n28,
            n29,
            n30
     from t1;
   type tp is table of c%rowtype;
   arr   tp;
 begin
   open c;
   fetch c   bulk collect into arr;
   close c;
   dbms_output.put_line(arr(9).n9);
 end;
 /
 /
 
 declare
   cursor c is
     select t.value.n1 n1,
            t.value.n2 n2,
            t.value.n3 n3,
            t.value.n4 n4,
            t.value.n5 n5,
            t.value.n6 n6,
            t.value.n7 n7,
            t.value.n8 n8,
            t.value.n9 n9,
            t.value.n10 n10,
            t.value.n11 n11,
            t.value.n12 n12,
            t.value.n13 n13,
            t.value.n14 n14,
            t.value.n15 n15,
            t.value.n16 n16,
            t.value.n17 n17,
            t.value.n18 n18,
            t.value.n19 n19,
            t.value.n20 n20,
            t.value.n21 n21,
            t.value.n22 n22,
            t.value.n23 n23,
            t.value.n24 n24,
            t.value.n25 n25,
            t.value.n26 n26,
            t.value.n27 n27,
            t.value.n28 n28,
            t.value.n29 n29,
            t.value.n30 n30
     from (select treat(t.value as o_30) value
           from t2 t
           where t.value is of type (o_30)) t;
   type tp is table of c%rowtype;
   arr   tp;
 begin
   open c;
   fetch c   bulk collect into arr;
   close c;
   dbms_output.put_line(arr(9).n9);
 end;
 /
 /
 
 set timing off

Final thing to mention is that this solution can be implemented on any Oracle version which supports inheritance. As far as I remember, inheritance was introduced in Oracle 9i, so the solution has already been available for two decades and it is not a new feature by any means.

Leave a comment