Як генерувати серію 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1,… у стандартних SQL або T-SQL?


11

Дано два числа nі m, я хочу , щоб створити ряд виду

1, 2, ..., (n-1), n, n, (n-1), ... 2, 1

і повторити це mраз.

Наприклад, для n = 3і m = 4я хочу послідовність наступних 24 чисел:

1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1, 1, 2, 3, 3, 2, 1
----------------  ----------------  ----------------  ----------------

Я знаю, як досягти цього результату в PostgreSQL одним із двох методів:

Використовуючи наступний запит, який використовує generate_seriesфункцію, та кілька прийомів, щоб гарантувати, що замовлення є правильним:

WITH parameters (n, m) AS
(
    VALUES (3, 5)
)
SELECT 
    xi
FROM
(
    SELECT
        i, i AS xi
    FROM
        parameters, generate_series(1, parameters.n) AS x(i)
    UNION ALL
    SELECT
        i + parameters.n, parameters.n + 1 - i AS xi
    FROM
        parameters, generate_series(1, parameters.n) AS x(i)
) AS s0 
CROSS JOIN 
    generate_series (1, (SELECT m FROM parameters)) AS x(j)
ORDER BY
    j, i ;

... або використовувати функцію з тією ж метою з суміжними та вкладеними петлями:

CREATE FUNCTION generate_up_down_series(
    _elements    /* n */ integer,
    _repetitions /* m */ integer)
RETURNS SETOF integer AS
$BODY$
declare
    j INTEGER ;
    i INTEGER ;
begin
    for j in 1 .. _repetitions loop
        for i in         1 .. _elements loop
              return next i ;
        end loop ;
        for i in reverse _elements .. 1 loop
              return next i ;
        end loop ;
    end loop ;
end ;
$BODY$
LANGUAGE plpgsql IMMUTABLE STRICT ;

Як я можу зробити еквівалент або в стандартному SQL, або в Transact-SQL / SQL Server?

Відповіді:


4

У Postgres легко використовувати generate_series()функцію:

WITH 
  parameters (n, m) AS
  ( VALUES (3, 5) )
SELECT 
    CASE WHEN g2.i = 1 THEN gn.i ELSE p.n + 1 - gn.i END AS xi
FROM
    parameters AS p, 
    generate_series(1, p.n) AS gn (i),
    generate_series(1, 2)   AS g2 (i),
    generate_series(1, p.m) AS gm (i)
ORDER BY
    gm.i, g2.i, gn.i ;

У стандартному SQL - і якщо припустити, що існує розмір розумного розміру параметрів n, m, тобто менше мільйона - ви можете використовувати Numbersтаблицю:

CREATE TABLE numbers 
( n int not null primary key ) ;

заповніть його бажаним методом вашої СУБД:

INSERT INTO numbers (n)
VALUES (1), (2), .., (1000000) ;  -- some mildly complex SQL here
                                  -- no need to type a million numbers

а потім використовувати його замість generate_series():

WITH 
  parameters (n, m) AS
  ( VALUES (3, 5) )
SELECT 
    CASE WHEN g2.i = 1 THEN gn.i ELSE p.n + 1 - gn.i END AS xi
FROM
    parameters AS p
  JOIN numbers AS gn (i) ON gn.i <= p.n
  JOIN numbers AS g2 (i) ON g2.i <= 2
  JOIN numbers AS gm (i) ON gm.i <= p.m 
ORDER BY
    gm.i, g2.i, gn.i ;

На практиці я не очікую, що ці цифри будуть більшими за 100; але теоретично вони можуть бути будь-якими.
joanolo

10

Постгрес

Ви можете змусити його працювати з єдиною generate_series() та базовою математикою (див. Математичні функції ).

Згорнутий у просту функцію SQL:

CREATE OR REPLACE FUNCTION generate_up_down_series(n int, m int)
  RETURNS SETOF int AS
$func$
SELECT CASE WHEN n2 < n THEN n2 + 1 ELSE n*2 - n2 END
FROM  (
   SELECT n2m, n2m % (n*2) AS n2
   FROM   generate_series(0, n*2*m - 1) n2m
   ) sub
ORDER  BY n2m
$func$  LANGUAGE sql IMMUTABLE;

Виклик:

SELECT * FROM generate_up_down_series(3, 4);

Породжує бажаний результат. n і m може бути будь-яким цілим числом, де n * 2 * m не переливається int4.

Як?

У підзапиті:

  • Створіть бажану загальну кількість рядків ( n * 2 * m ), простим висхідним числом. Я його називаю n2m. Від 0 до N-1 (не від 1 до N ) для спрощення наступних операцій модуля .

  • Візьміть % n * 2 ( %це оператор модуля), щоб отримати ряд з n зростаючих чисел, m разів. Я його називаю n2.

У зовнішньому запиті:

  • Додати нижню половину ( n2 <n ).

  • Для верхньої половини ( n2> = n ) дзеркало нижньої половини з n * 2 - n2 .

  • Я додав, ORDER BYщоб гарантувати запитуване замовлення. З поточними версіями або Postgres він також працює без ORDER BYпростого запиту - але не обов'язково в більш складних запитах! Це деталізація щодо впровадження (і вона не збирається змінюватися), але не гарантована стандартом SQL.

На жаль, чи generate_series()є Postgres специфічним і не стандартним SQL, як це було прокоментовано. Але ми можемо повторно використовувати ту саму логіку:

Стандартний SQL

Ви можете генерувати серійні номери з рекурсивним CTE замість generate_series()або, що більш ефективно для повторного використання, створити таблицю з порядковими цілими числами один раз. Кожен може читати, до нього ніхто не може писати!

CREATE TABLE int_seq (i integer);

WITH RECURSIVE cte(i) AS (
   SELECT 0
   UNION ALL
   SELECT i+1 FROM cte
   WHERE  i < 20000  -- or as many you might need!
   )
INSERT INTO int_seq
SELECT i FROM cte;

Тоді вищесказане SELECTстає ще простішим:

SELECT CASE WHEN n2 < n THEN n2 + 1 ELSE n*2 - n2 END AS x
FROM  (
   SELECT i, i % (n*2) AS n2
   FROM   int_seq
   WHERE  i < n*2*m  -- remember: 0 to N-1
   ) sub
ORDER  BY i;

5

Якщо вам потрібен звичайний SQL. Теоретично він повинен працювати над більшістю СУБД (тестовано на PostgreSQL та SQLite):

with recursive 
  s(i,n,z) as (
    select * from (values(1,1,1),(3*2,1,2)) as v  -- Here 3 is n
    union all
    select
      case z when 1 then i+1 when 2 then i-1 end, 
      n+1,
      z 
    from s 
    where n < 3), -- And here 3 is n
  m(m) as (select 1 union all select m+1 from m where m < 2) -- Here 2 is m

select n from s, m order by m, i;

Пояснення

  1. Утворіть ряд 1..n

    Якщо припустити, що n=3

    with recursive s(n) as (
      select 1
      union all
      select n+1 from s where n<3
    )
    select * from s;

    Це досить просто і його можна знайти майже в будь-яких документах про рекурсивні CTE. Однак для цього потрібно два екземпляри кожного значення

  2. Утворіть ряд 1,1, .., n, n

    with recursive s(n) as (
      select * from (values(1),(1)) as v
      union all
      select n+1 from s where n<3
    )
    select * from s;

    Тут ми просто подвоюємо початкове значення, яке має два ряди, але другий зв'язок нам потрібен у зворотному порядку, тому ми введемо порядок трохи.

  3. Перш ніж ввести порядок, зауважте, що це теж річ. У нас може бути два ряди в початковому стані з трьома стовпцями кожен, наш n<3все ще є одним стовпцем умовно. І ми все ще просто збільшуємо цінність n.

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(1,1,1)) as v
      union all
      select
        i,
        n+1,
        z 
      from s where n<3
    )
    select * from s;
  4. Так само ми можемо їх трохи змішати, спостерігаючи, як тут змінюються початкові умови : тут ми маємо (6,2),(1,1)

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(6,1,2)) as v
      union all
      select
        i,
        n+1,
        z 
      from s where n<3
    )
    select * from s;
  5. Утворіть ряд 1..n, n..1

    Хитрість тут полягає в тому, щоб генерувати серію (1..n) двічі, а потім просто змінити впорядкування на другому наборі.

    with recursive s(i,n,z) as (
      select * from (values(1,1,1),(3*2,1,2)) as v
      union all
      select
        case z when 1 then i+1 when 2 then i-1 end, 
        n+1,
        z 
      from s where n<3
    )
    select * from s order by i;

    Ось iпорядок і zномер послідовності (або половина послідовності, якщо ви хочете). Отже, для послідовності 1 ми збільшуємо порядок від 1 до 3, а для послідовності 2 зменшуємо порядок з 6 до 4. І нарешті

  6. Помножте ряд на m

    (див. перший запит у відповіді)


3

Якщо ви хочете портативне рішення, вам потрібно зрозуміти, що це в основному математична проблема.

Враховуючи @n як найбільшу кількість послідовності і @x як позицію числа в цій послідовності (починаючи з нуля), у SQL Server працює наступна функція:

CREATE FUNCTION UpDownSequence
(
    @n int, -- Highest number of the sequence
    @x int  -- Position of the number we need
)
RETURNS int
AS
BEGIN
    RETURN  @n - 0.5 * (ABS((2*((@x % (@n+@n))-@n)) +1) -1)
END
GO

Ви можете перевірити це за допомогою CTE:

DECLARE @n int=3;--change the value as needed
DECLARE @m int=4;--change the value as needed

WITH numbers(num) AS (SELECT 0 
                      UNION ALL
                      SELECT num+1 FROM numbers WHERE num+1<2*@n*@m) 
SELECT num AS Position, 
       dbo.UpDownSequence(@n,num) AS number
FROM numbers
OPTION(MAXRECURSION 0)

(Швидке пояснення: функція використовує MODULO () для створення послідовності повторюваних чисел, а ABS () для перетворення її в зигзагоподібну хвилю. Інші операції перетворюють цю хвилю на відповідну бажаному результату.)


2

У PostgreSQL це легко,

CREATE OR REPLACE FUNCTION generate_up_down_series(n int, m int)
RETURNS setof int AS $$
SELECT x FROM (
  SELECT 1, ordinality AS o, x FROM generate_series(1,n) WITH ORDINALITY AS t(x)
  UNION ALL
  SELECT 2, ordinality AS o, x FROM generate_series(n,1,-1) WITH ORDINALITY AS t(x)
) AS t(o1,o2,x)
CROSS JOIN (
  SELECT * FROM generate_series(1,m)
) AS g(y)
ORDER BY y,o1,o2
$$ LANGUAGE SQL;

2

Це працює в MS-SQL, і я думаю, що його можна змінити на будь-який смак SQL.

declare @max int, @repeat int, @rid int

select @max = 3, @repeat = 4

-- create a temporary table
create table #temp (row int)

--create seed rows
while (select count(*) from #temp) < @max * @repeat * 2
begin
    insert into #temp
    select 0
    from (values ('a'),('a'),('a'),('a'),('a')) as a(col1)
    cross join (values ('a'),('a'),('a'),('a'),('a')) as b(col2)
end

-- set row number can also use identity
set @rid = -1

update #temp
set     @rid = row = @rid + 1

-- if the (row/max) is odd, reverse the order
select  case when (row/@max) % 2 = 1 then @max - (row%@max) else (row%@max) + 1 end
from    #temp
where   row < @max * @repeat * 2
order by row

2

Спосіб зробити це в SQL Server за допомогою рекурсивного cte.

1) Створіть необхідну кількість членів у ряді (для n = 3 та m = 4 це було б 24, що дорівнює 2 * n * m)

2) Після цього, використовуючи логіку в caseвиразі, ви можете генерувати необхідний ряд.

Sample Demo

declare @n int=3;--change the value as needed
declare @m int=4;--change the value as needed

with numbers(num) as (select 1 
                      union all
                      select num+1 from numbers where num<2*@n*@m) 
select case when (num/@n)%2=0 and num%@n<>0 then num%@n 
            when (num/@n)%2=0 and num%@n=0 then 1  
            when (num/@n)%2=1 and num%@n<>0 then @n+1-(num%@n)  
            when (num/@n)%2=1 and num%@n=0 then @n
       end as num
from numbers
OPTION(MAXRECURSION 0)

Як запропонував @AndriyM .. caseвираз можна спростити до

with numbers(num) as (select 0
                      union all
                      select num+1 from numbers where num<2*@n*@m-1) 
select case when (num/@n)%2=0 then num%@n + 1
            when (num/@n)%2=1 then @n - num%@n
       end as num
from numbers
OPTION(MAXRECURSION 0)

Demo


2

Використовуючи лише основні математику + - * /та модуль:

SELECT x
    , s = x % (2*@n) +
         (1-2*(x % @n)) * ( ((x-1) / @n) % 2)
FROM (SELECT TOP(2*@n*@m) x FROM numbers) v(x)
ORDER BY x;

Для цього не потрібно конкретного SGBD.

Маючи numbersтаблицю чисел:

...; 
WITH numbers(x) AS(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
    FROM (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n0(x)
    CROSS JOIN (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n1(x)
    CROSS JOIN (VALUES(0), (0), (0), (0), (0), (0), (0), (0), (0), (0)) AS n2(x)
)
...

Це генерує таблицю чисел (1-1000) без використання рекурсивного CTE. Дивіться зразок . 2 * n * m має бути меншим за кількість рядків у числах.

Вихід з n = 3 і m = 4:

x   s
1   1
2   2
3   3
4   3
5   2
6   1
7   1
8   2
... ...

Ця версія вимагає меншої таблиці чисел (v> = n і v> = m):

WITH numbers(v) AS(
    SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
    FROM (VALUES(1), (2), (3), (4), (5), (6), ...) AS n(x)
)
SELECT ord = @n*(v+2*m) + n
    , n*(1-v) + ABS(-@n-1+n)*v
FROM (SELECT TOP(@n) v FROM numbers ORDER BY v ASC) n(n)
CROSS JOIN (VALUES(0), (1)) AS s(v)
CROSS JOIN (SELECT TOP(@m) v-1 FROM numbers ORDER BY v ASC) m(m)
ORDER BY ord;

Дивіться зразок .


2

Основна функція за допомогою ітераторів.

T-SQL

create function generate_up_down_series(@max int, @rep int)
returns @serie table
(
    num int
)
as
begin

    DECLARE @X INT, @Y INT;
    SET @Y = 0;

    WHILE @Y < @REP
    BEGIN

        SET @X = 1;
        WHILE (@X <= @MAX)
        BEGIN
            INSERT @SERIE
            SELECT @X;
            SET @X = @X + 1;
        END

        SET @X = @MAX;
        WHILE (@X > 0)
        BEGIN
            INSERT @SERIE
            SELECT @X;
            SET @X = @X -1;
        END

        SET @Y = @Y + 1;
    END

    RETURN;
end
GO

Постгрес

create or replace function generate_up_down_series(maxNum int, rep int)
returns table (serie int) as
$body$
declare
    x int;
    y int;
    z int;
BEGIN

    x := 0;
    while x < rep loop

        y := 1;
        while y <= maxNum loop
            serie := y;
            return next;
            y := y + 1;
        end loop;

        z := maxNum;
        while z > 0 loop
            serie := z;
            return next;
            z := z - 1;
        end loop;

        x := x + 1;
    end loop;

END;
$body$ LANGUAGE plpgsql;

1
declare @n int = 5;
declare @m int = 3;
declare @t table (i int, pk int identity);
WITH  cte1 (i) 
AS ( SELECT 1
     UNION ALL
     SELECT i+1 FROM cte1
     WHERE  i < 100  -- or as many you might need!
   )
insert into @t(i) select i from cte1 where i <= @m  order by i
insert into @t(i) select i from @t order by i desc
select t.i --, t.pk, r.pk 
from @t as t 
cross join (select pk from @t where pk <= @n) as r
order by r.pk, t.pk
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.