Go to content Go to navigation and search

Home

Current Oracle Spatial Blogs

Search

RSS / Atom

Email me

textpattern

generate_series: an Oracle implementation in light of SQL Design Patterns

· Nov 6, 04:47 pm by Simon Greener

There is a very useful function in PostgreSQL called generate_series that can be used to generate a series of integer numbers from som start value to and end value with an optional step value.

Here is the function and its description from the PostgreSQL help.

Function Argument Type Return Type Description
generate_series(start, stop) int or bigint setof int or setof bigint (same as argument type) Generate a series of values, from start to stop with a step size of one
generate_series(start, stop, step) int or bigint setof int or setof bigint (same as argument type) Generate a series of values, from start to stop with a step size of step

There are a number of ways we can code this function in Oracle. Since the original function is a “set returning function”, we need to code generate_series so that it returns a table of numbers:

TYPE t_numbers IS TABLE OF number;

The most efficient way to do this is via a PIPELINED function, so that is what I will code.

create or replace
 function generate_series(p_start in pls_integer,
                           p_end   in pls_integer,
                           p_step  in pls_integer := 1 )
       Return CODESYS.centroid.t_numbers Pipelined
  As
    v_i    PLS_INTEGER := CASE WHEN p_start IS NULL THEN 1 ELSE p_start END;
    v_step PLS_INTEGER := CASE WHEN p_step IS NULL OR p_step = 0 THEN 1 ELSE p_step END;
    v_terminating_value PLS_INTEGER :=  p_start + TRUNC(ABS(p_start-p_end) / abs(v_step) ) * v_step;
  Begin
     -- Check for impossible combinations
     If ( p_start > p_end AND SIGN(p_step) = 1 )
        Or
        ( p_start < p_end AND SIGN(p_step) = -1 ) Then
       Return;
     End If;
     -- Generate integers
     LOOP
       PIPE ROW ( v_i );
       EXIT WHEN ( v_i = v_terminating_value );
       v_i := v_i + v_step;
     End Loop;
     Return;
  End generate_series;
/
show errors

Now, to run the tests on the PostgreSQL help page:

Let’s start with a simple, additional, example not on the page.

SQL> select g.column_value as generate_series
  2*   from table(generate_series(1,5)) g;

GENERATE_SERIES
----------------------
1
2
3
4
5

5 rows selected

Now, let’s execute the ones on the help page.

SQL> select g.column_value as generate_series
  2*   from table(generate_series(2,4)) g

GENERATE_SERIES
---------------
              2
              3
              4

3 rows selected.

SQL> select g.column_value as generate_series
  2*   from table(generate_series(5,1,-2)) g

GENERATE_SERIES
---------------
              5
              3
              1

3 rows selected.

SQL> select g.column_value as generate_series
  2*  from table(generate_series(4,3)) g

no rows selected

With one additional:

SQL> select g.column_value as generate_series
  2*   from table(generate_series(-4,-1,1)) g

GENERATE_SERIES
---------------
             -4
             -3
             -2
             -1

4 rows selected.

And finally.

SQL> select to_char(current_date + sa.column_value,'YYYY-MM-DD') as da
  2*   from table(generate_series(0,14,7)) sa

     DATES
----------
2008-11-06
2008-11-13
2008-11-20

3 rows selected.

Alternative Table Function

Now all this is very good, but there is some debate as to implementing a series of integers in this way.

Vadim Tropashko, in his excellent book, “SQL Design Patterns, The Expert Guide to SQL Programming”, Rampart Press has a whole chapter (2) devoted to “Integer Generators in SQL”. In this chapter, Vadmin presents an coding of a simple Integer generating table function called “Integers”. His coding is as follows.

create or replace function Integers
       return t_integers pipelined
As
Begin
   Loop
     PIPE ROW ( 0 );
   End Loop;
   Return;
end Integers;

We will now use this function to implement the PostgreSQL help examples above.

Firstly, generating numbers between 1 and 5.

SQL> select rownum as rin
  2    from table(Integers)
  3*  where rownum <= 5

       RIN
----------
         1
         2
         3
         4
         5

5 rows selected.

All numbers between 2 and 4.

SQL> select rin
  2    from (select rownum as rin
  3            from table(Integers)
  4           where rownum <= 5)
  5*  where rin between 2 and 4

       RIN
----------
         2
         3
         4

2 rows selected.

Series 5,3,1 using a step of -2.

SQL> select rin
  2    from (select 5 + ((rownum-1) * -1) as rin
  3            from table(Integers)
  4           where rownum < 10)
  5   where rin between 1 and 5
  6*    and mod(rin,ABS(-2)) = 1;

       RIN
----------
         5
         3
         1

3 rows selected

I won’t code the invalid series. So let’s move on to the negative series.

SQL> select rin
  2    from (select -1 + (rownum - 1 ) * -1 as rin
  3            from table(Integers)
  4           where rownum < 10
  5           order by 1)
  6*  where rin between -4 and -1;

       RIN
----------
        -4
        -3
        -2
        -1

4 rows selected

And finally.

SQL> select to_char(current_date + sa.rin,'YYYY-MM-DD') as dates 
  2    from (select (rownum - 1) as rin
  3            from table(Integers)
  4           where rownum <= 15) sa
  5*  where mod(rin,7) = 0;

     DATES
----------
2008-11-06
2008-11-13
2008-11-20

3 rows selected.

Hierarchical Queries

Vadim goes on in his book to describe the use of hierarchical queries for generating integer series. Here is an example of how to generate all even numbers between 5 and 8.

SQL> select level
  2    from dual
  3   where level between 5 and 8
  4     and MOD(level,2) = 0
  5*   connect by level <= 10

     LEVEL
----------
         6
         8

2 rows selected.

And the negative series -4,-3,-2,-1 above.

SQL> select level * -1
  2    from dual
  3   where level between 1 and 4
  4   connect by level <= 5
  5*  order by level desc;

     LEVEL
----------
        -4
        -3
        -2
        -1

4 rows selected

I am a big fan of hierachical queries and have used them a lot in my work but mainly in the area of generating sample data (see other articles in this blog for examples). My personal view is that coding integer ranges with the “connect by level” hierarchical query is simpler to use and code than the Integers() function. However, the “connect by level” usage above does not work in versions of Oracle before 10g. So all you 9i users will have to resort to coding a suitable table function.

Conclusion

Tropashko’s preference (as also Mikito Harakiri) in coding an integer series generation table function is to code one without parameters, such as Integers(), and not like generate_series. The main reason is that he prefers the use of predicates in SQL SELECT statements (such as “rownum < = 1000”) than parameters passed to a function. This is because they are at a “higher abstraction level”, claiming that “programs with predicate expressions are shorter and cleaner”. Because I am a “SQL man”, prefering a single SQL statement to complex PL/SQL, and because I like things to be done “orthogonally”, “theoretically” and via patterns/templates I am tempted to agree with Tropashko. However, when I look at the SQL above, I find the generate_series implementation much, much cleaner and less complicated.

The problem with the Integers() function is that it forces me to “reinvent the wheel” each time I need a series of integers. With simple series the SQL Is not too bad. But more complicated series create much more complicated SQL. Now, many of my uses of SQL result in large SQL statements with can result in many uses of generate_series. To integrate multiple calls to the same series generation can be neatly encapsulated using the WITH construct, but even so it does add, to my way of thinking, unnecessary complicated. And complication usually means lots of errors and longer debugging.

In the end a generate_series function like above allows us to design and algorithm, code and test it and then use it many times without having to remember how to code an integer series every time we need it. This is part of good software engineering: the algorithm is “encapsulated” in a program unit that offers a stable interface. And interfaces are everthing.

post this at del.icio.uspost this at Diggpost this at Technoratipost this at Newsvinepost this at Ma.gnoliapost this at Furlpost this at Blinklistpost this at Spurlpost this at Wistspost this at Simpypost this at Redditpost this at Farkpost this at Blogmarkspost this at Yahoo! my webpost this at Mr. Wongpost this at Windows Livepost this at Google Bookmarkspost this to Twitter
  1. Simon,
    Neat. I’m tempted to do the same thing for SQL Server since its a pattern I use often especially for things that don’t lend themselves to standard SQL constructs like figuring out length of stays for homeless and hospitalization for claims analysis. I’m just not sure what the performance will be like.

    In general I guess the size of the sets I generally use it for shouldn’t impact performance much.


    Regina    Nov 12, 01:47 am    #
  2. Regina,

    Thanks for commenting: I am a bit fan of your work on "PostGIS":http://postgis.refractions.net.

    I have risen to the challenge and ported my version of generate_series to SQL Server 2008 T-SQL. I have tested it and it appears to work correctly.

    DROP FUNCTION [generate_series]
    GO
    
    CREATE FUNCTION [generate_series] ( @p_start INT, @p_end INT, @p_step INT=1 )
    RETURNS @Integers TABLE ( [IntValue] INT )
    AS
    BEGIN
        DECLARE @v_i                 INT;
        SET @v_i = CASE WHEN @p_start IS NULL THEN 1 ELSE @p_start END;
        DECLARE @v_step              INT;
        SET @v_step  = CASE WHEN @p_step IS NULL OR @p_step = 0 THEN 1 ELSE @p_step END;
        DECLARE @v_terminating_value INT;
        SET @v_terminating_value =  @p_start + CONVERT(INT,ABS(@p_start-@p_end) / ABS(@v_step) ) * @v_step;
    
         -- Check for impossible combinations
         IF NOT ( ( @p_start > @p_end AND SIGN(@p_step) = 1 )
                  OR
                  ( @p_start < @p_end AND SIGN(@p_step) = -1 )) 
         BEGIN
           WHILE ( 1 = 1 )
           BEGIN
               INSERT INTO @Integers ( [IntValue] ) VALUES ( @v_i )
               IF ( @v_i = @v_terminating_value )
                  BREAK
               SET @v_i = @v_i + @v_step;
           END;
         END;
        RETURN
    END
    GO
    
    SELECT g.IntValue
      FROM [generate_series] ( 100, 500, 10 ) g;
    
    REM Unlike stored procedures, User Defined Functions (UDFs) require that all parameters 
    REM be specified when calling them, even if default values for those parameters are declared. 
    REM So, if we want to use a parameter's default value, one must use the DEFAULT T-SQL keyword 
    REM instead of a parameter value. In this situation, this rather defeats the purpose of 
    REM having a default parameter as it is easier to type 1 than DEFAULT!!!
    REM
    SELECT g.IntValue as generate_series 
      FROM generate_series(1,5,DEFAULT) g;
    
    SELECT g.IntValue as generate_series 
      FROM generate_series(2,4,1) g;
    
    SELECT g.IntValue as generate_series 
      FROM generate_series(5,1,-2) g;
    
    SELECT g.IntValue as generate_series 
     FROM generate_series(4,3,1) g;
    
    SELECT g.IntValue as generate_series 
      FROM generate_series(-4,-1,1) g;
    
    SELECT convert(varchar(20),GETDATE() + sa.IntValue,112) as dates 
      FROM generate_series(0,14,7) sa;
    
    I hope this is useful. Let me know if you want me to look at a version of the "Integers()" function and how to use it in ordinary SQL.

    regards
    Simon
    Simon Greener    Nov 14, 09:59 am    #
  Textile Help