4

I want to display all the numbers (even / odd / mixed) between two numbers (1-9; 2-10; 11-20) in one (or two) column.
Example initial data:

| rang  |              | r1 | r2 |
--------               -----|-----
| 1-9   |              | 1  | 9  |
| 2-10  |              | 2  | 10 |
| 11-20 |      or      | 11 | 20 |

CREATE TABLE initialtableone(rang TEXT);
INSERT INTO initialtableone(rang) VALUES
  ('1-9'),
  ('2-10'),
  ('11-20');

CREATE TABLE initialtabletwo(r1 NUMERIC, r2 NUMERIC);
INSERT INTO initialtabletwo(r1, r2) VALUES
  ('1', '9'),
  ('2', '10'),
  ('11', '20');

Result:

| output                         |
----------------------------------
| 1,3,5,7,9                      |
| 2,4,6,8,10                     |
| 11,12,13,14,15,16,17,18,19,20  |
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
spatialhast
  • 261
  • 5
  • 19

2 Answers2

4

Something like this:

create table ranges (range varchar);

insert into ranges 
values
('1-9'),
('2-10'),
('11-20');

with bounds as (
  select row_number() over (order by range) as rn,
         range,
         (regexp_split_to_array(range,'-'))[1]::int as start_value,
         (regexp_split_to_array(range,'-'))[2]::int as end_value
  from ranges
)
select rn, range, string_agg(i::text, ',' order by i.ordinality)
from bounds b
  cross join lateral generate_series(b.start_value, b.end_value) with ordinality i
group by rn, range

This outputs:

rn | range | string_agg                   
---+-------+------------------------------
 3 | 2-10  | 2,3,4,5,6,7,8,9,10           
 1 | 1-9   | 1,2,3,4,5,6,7,8,9            
 2 | 11-20 | 11,12,13,14,15,16,17,18,19,20
2

Building on your first example, simplified, but with PK:

CREATE TABLE tbl1 (
  tbl1_id serial PRIMARY KEY  -- optional
, rang text  -- can be NULL ?
);

Use split_part() to extract lower and upper bound. (regexp_split_to_array() would be needlessly expensive and error-prone). And generate_series() to generate the numbers.

Use a LATERAL join and aggregate the set immediately to simplify aggregation. An ARRAY constructor is fastest in this case:

SELECT t.tbl1_id, a.output  -- array; added id is optional
FROM  (
   SELECT tbl1_id
        , split_part(rang, '-', 1)::int AS a
        , split_part(rang, '-', 2)::int AS z
   FROM   tbl1
   ) t
 , LATERAL (
   SELECT ARRAY(  -- preserves rows with NULL
      SELECT g FROM generate_series(a, z, CASE WHEN (z-a)%2 = 0 THEN 2 ELSE 1 END) g
      ) AS output
   ) a;

AIUI, you want every number in the range only if upper and lower bound are a mix of even and odd numbers. Else, only return every 2nd number, resulting in even / odd numbers for those cases. This expression implements the calculation of the interval:

CASE WHEN (z-a)%2 = 0 THEN 2 ELSE 1 END

Result as desired:

output
-----------------------------
1,3,5,7,9
2,4,6,8,10
11,12,13,14,15,16,17,18,19,20

You do not need WITH ORDINALITY in this case, because the order of elements is guaranteed.

The aggregate function array_agg() makes the query slightly shorter (but slower) - or use string_agg() to produce a string directly, depending on your desired output format:

SELECT a.output  -- string
FROM  (
   SELECT split_part(rang, '-', 1)::int AS a
        , split_part(rang, '-', 2)::int AS z
   FROM   tbl1
   ) t
, LATERAL (
      SELECT string_agg(g::text, ',') AS output
      FROM   generate_series(a, z, CASE WHEN (z-a)%2 = 0 THEN 2 ELSE 1 END) g
   ) a;

Note a subtle difference when using an aggregate function or ARRAY constructor in the LATERAL subquery: Normally, rows with rang IS NULLare excluded from the result because the LATERAL subquery returns no row.
If you aggregate the result immediately, "no row" is transformed to one row with a NULL value, so the original row is preserved. I added demos to the fiddle.

SQL Fiddle.

You do not need a CTE for this, which would be more expensive.

Aside: The type conversion to integer removes leading / training white space automatically, so a string like this works as well for rank: ' 1 - 3'.

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Thanks this is helpfull ... wondering if there is any option of using range data types to create ranges with steps to create odd or even or mixed ranges something like range(lower-bound,upper-bound, step) with Built-in Range Types – Jorge Vidinha Feb 27 '18 at 20:08