SQL-запрос для комбинаций без повторения для первых N случаев - PullRequest
1 голос
/ 04 октября 2019

Мне нужен запрос, который можно использовать в функции (или в качестве функции) и получить из таблицы N комбинаций значений М.

Пример: Ввод: таблица со значениями в одном столбце в нескольких строках

Корпус 1

N=2
M=4 (Record1 to Record4)

Таблица

Record1
Record2
Record3
Record4

Выход

Record1
Record2
Record3
Record4
Record1,Record2
Record1,Record3
Record1,Record4
Record2,Record3
Record2,Record4
Record3,Record4

Корпус 2

N=3
M=4 (Record1 to Record4)

Таблица

Record1
Record2
Record3
Record4

Вывод

Record1
Record2
Record3
Record4
Record1,Record2
Record1,Record3
Record1,Record4
Record2,Record3
Record2,Record4
Record3,Record4
Record1,Record2,Record3
Record1,Record2,Record4
Record1,Record3,Record4
Record2,Record3,Record4

Я использую этот вопрос в качестве базового кода для выполнения

Ответы [ 3 ]

3 голосов
/ 05 октября 2019

Если требуется только фиксированное количество N значений на комбинацию, то это легко сделать в обычном SQL.

Просто с помощью N-1 самосоединений.

Например, если N = 3, то 2 самосоединения:

SELECT 
CONCAT(t1.name, ',', t2.name, ',', t3.name) AS names
FROM yourtable t1
JOIN yourtable t2 ON t2.name > t1.name
JOIN yourtable t3 ON t3.name > t2.name;

Из-за использования > в соединениях, которые не будут возвращать дубликаты одинаковых комбинаций в другом порядке.
(Так как A,B,C = A,C,B = B,A,C = B,C,A = C,A,B = C,B,A)

Если N переменная, то такой метод можно использовать в динамическом Sql, который добавляет N-1 объединений в строку запроса.

Однако, чтобы получить ожидаемый результат вопроса, нужно вернутьтакже N = 1 & N = 2 & N = 3, тогда мы могли бы использовать этот трюк в сочетании с рекурсивным CTE.

Например, этот фрагмент T-SQL:

declare @yourtable table ([name] varchar(30));
insert into @yourtable ([name]) values 
('Record1'), 
('Record2'), 
('Record3'), 
('Record4');

WITH RCTE AS 
(
    SELECT 1 as N, t.name as prev_name, cast(t.name as varchar(max)) AS names
    FROM @yourtable t

    UNION ALL

    SELECT N + 1, t.name, CONCAT(r.names,','+ t.name)
    FROM @yourtable t
    JOIN RCTE r ON t.name > r.prev_name AND r.N < 3
)
SELECT names
FROM RCTE
ORDER BY N, names;

Возвращает:

names
------------------------
Record1
Record2
Record3
Record4
Record1,Record2
Record1,Record3
Record1,Record4
Record2,Record3
Record2,Record4
Record3,Record4
Record1,Record2,Record3
Record1,Record2,Record4
Record1,Record3,Record4
Record2,Record3,Record4
2 голосов
/ 05 октября 2019

Здесь я реализовал алгоритм обхода путем комбинации имен в алфавитном порядке до N имен. Алгоритм не пересекает все комбинации 2^N-1, как в упомянутом базовом коде .

Алгоритм объясняется следующим образом. Обозначение i-th имени представлено цифрой i (начиная с 1), тогда комбинация имен может быть представлена ​​последовательностью строго возрастающих цифр. Строго возрастающее ограничение исключает различные перестановки из одной и той же комбинации.

Теперь мы можем пройти через эти представления в алфавитном порядке, как показано ниже (как в реальном словаре). Можно ясно видеть, что есть 2 ключа для обхода: (1) , когда делать округление и (2) , выполняя это с учетом строго возрастающего ограничения . Остальное - правильно их реализовать в коде.

Отказ от ответственности: Низкая эффективность, ремонтопригодность и масштабируемость! Он едва работает на небольшом наборе данных (38 секунд для 1350 комбинаций из M=20 и N=3). Рассмотрите выполнение этой задачи на языке программирования общего назначения, особенно когда набор данных большой. например, Pythonic решение только с 4 строками

Понятие того, что делают алгоритмы

Combination of names
---------------------
1      -- 1 means 'name1'
2
...
n      -- Round up to 2-digit combinations after this number
1,2    -- The lowest digit begins at 2 because (1,1) violates the constraint
1,3
...
1,n
2,3    -- begins at 3 because 2,1 and 2,2 violates the constraint
2,4
....
2,n
.....
(n-1),n  -- Can't round up to (n,n) so here ends the combinations of 2 digits.
1,2,3    -- In general, round-up gives a consecutive increasing sequence 
         -- starting from the rounding digit
1,2,4
....     -- repeat until all wanted combinations are exhausted. 

А вот код T-SQL

Набор тестовых данных и параметры

use [testdb];
if OBJECT_ID('testdb..test') is not null
    drop table testdb..test;

create table test (
    [name] VARCHAR(10)
);
GO

-- name list, need not be sorted
insert into test([name]) 
values ('name1'),('name2'),('name3'),('name4'),('name5');

-- Number of names used in the combination
declare @N int = 3;  

Программа

/***** temp variables *****/

-- get number of names
declare @M int;
select @M = count(distinct [name]) from test;
--print @M;

-- array (table variable) of temporary status
declare @arr table (
    digit int primary key,
    val int,
    [name] varchar(max)
)
-- the value of each digits begins from 0
insert @arr(digit, val, [name])
select ROW_NUMBER() over(order by [name]), 0, [name] from test;

-- begin traversal from the first name
update @arr
    set val = 1
    where digit = 1;
-- check:
-- select * from @arr;

-- concatenation of names
DECLARE @cat VARCHAR(max) = ''

-- current value at the lowest digit
declare @val_now int = 1;  -- 1 to N

-- digit pointer and temporary value used for rounding
declare @digit_round int;
declare @val_round int;

-- answer storage
if OBJECT_ID('tempdb..#ans') is not null
    drop table #ans;
create table #ans (
    names varchar(max)
)

/***** Main Loop *****/

declare @flag_terminate int = 0;

while (@flag_terminate = 0) begin
    while @val_now <= @M BEGIN

        /** output current combination **/

        -- select * from @arr;

        set @cat = '';
        -- use cursor to preserve name order
        declare @name_now varchar(100);    
        declare cur cursor local
        for select (select B.[name] from @arr as B where B.digit = A.val) as [name]
            from @arr as A
            where A.val > 0
            order by [name];
        open cur;

        while 1=1 begin
            fetch next from cur into @name_now;
            if @@FETCH_STATUS <> 0 break;
            set @cat = case @cat
                           when '' then ''
                           else @cat + ','
                       end + @name_now;
        end
        close cur;
        deallocate cur;

        -- Can also do this if you don't care about name order
        -- Note that 'order by' won't work on subqueries
        -- SELECT @cat = case @cat
        --                   when '' then ''
        --                   else @cat + ','
        --               end + (select B.[name] from @arr as B where B.digit = A.val)
        -- FROM @arr as A
        -- where A.val > 0;

        insert into #ans(names) values (@cat);

        /** Proceed to next combination **/

        -- 1. no round-up case
        if @val_now < @M begin
            set @val_now += 1;
            update @arr
                set val = @val_now
                where digit = 1;
        end
        -- 2. round-up case
        else begin

            -- Find which digit at where the rounding process should end
            set @digit_round = 1            
            while @digit_round <= @M begin
                -- N.B. Ascending alphabatical sorting of name combanitaions implies
                --      max(digit 2) = M - 1 
                --      max(digit 3) = M - 2
                --      ....
                --      max(digit N) = M - N + 1
                set @val_round = (select val from @arr where digit = @digit_round);
                if @val_round < @M - @digit_round + 1
                    break;
                set @digit_round += 1;
            end
            -- print '    digit_round = ' + str(@digit_round)
            -- print '    val_round = ' + str(@val_round)

            -- 2.1 regular round-up case
            --     rounding process ends within N digits
            if @digit_round <= @N begin

                while @digit_round >= 1 begin
                    -- rounded value
                    set @val_round += 1;
                    -- print 'start rounding:'
                    -- print '    digit_round = ' + str(@digit_round)
                    -- print '    val_round = ' + str(@val_round)

                    -- update temp table
                    update @arr
                        set val = @val_round
                        where digit = @digit_round;
                    -- next digit
                    set @digit_round -= 1;
                end
                -- reset loop vars
                set @val_now = @val_round;
            end
            -- 2.2 termination case
            --     rounding process does not end in N digits
            else begin
                -- print 'Termination reached!'
                set @flag_terminate = 1;
                set @val_now = @M + 1;
            end
        end
    end
end

-- output
select * from #ans;

Результат

|    | names             |
|----|-------------------|
| 1  | name1             |
| 2  | name2             |
| 3  | name3             |
| 4  | name4             |
| 5  | name5             |
| 6  | name1,name2       |
| 7  | name1,name3       |
| 8  | name1,name4       |
| 9  | name1,name5       |
| 10 | name2,name3       |
| 11 | name2,name4       |
| 12 | name2,name5       |
| 13 | name3,name4       |
| 14 | name3,name5       |
| 15 | name4,name5       |
| 16 | name1,name2,name3 |
| 17 | name1,name2,name4 |
| 18 | name1,name2,name5 |
| 19 | name1,name3,name4 |
| 20 | name1,name3,name5 |
| 21 | name1,name4,name5 |
| 22 | name2,name3,name4 |
| 23 | name2,name3,name5 |
| 24 | name2,name4,name5 |
| 25 | name3,name4,name5 |

Протестировано на SQL Server 2017, последняя версия (образ установщика Linux).

1 голос
/ 06 октября 2019

Что может быть максимальным значением N & M?

Я использую RBAR и dynamic Sql и CROSS JOIN.

DECLARE @N INT = 3
DECLARE @M INT = 4 --M=4 (Record1 to Record4)

-- Purpose of #temp
-- Insert your resultset in #temp using RowNumber or Top (where RowNumber=@M or Top(@M))
-- This is easy part 

CREATE TABLE #temp (
    id INT identity(1, 1)
    ,col VARCHAR(50)
    )

INSERT INTO #temp
VALUES ('Record1')
    ,('Record2')
    ,('Record3')
    ,('Record4')





DECLARE @i INT = 1
DECLARE @Sql NVARCHAR(500) = ''
DECLARE @Col NVARCHAR(500) = ''
DECLARE @From NVARCHAR(500)
DECLARE @Where NVARCHAR(500) = ''
DECLARE @Alias NVARCHAR(5)

WHILE (@i <= @N)
BEGIN
    SET @Alias = 't' + cast(@i AS VARCHAR)

    IF (@i = 1)
    BEGIN
        SET @Col = CONCAT (
                @Alias
                ,'.' + 'col'
                )
        SET @From = '#temp ' + @Alias
        SET @Sql = 'select ' + @Col + ' from  ' + @From + ''
    END
    ELSE IF (@i = 2)
    BEGIN
        SET @Col = @Col + '+'',''' + '+' + CONCAT (
                @Alias
                ,'.' + 'col'
                ,' '
                )
        SET @From = @From + ',' + '#temp ' + @Alias
        SET @Where = @Where + 't' + cast(@i - 1 AS VARCHAR) + '.id < t' + cast(@i AS VARCHAR) + '.id'
        SET @Sql = @Sql + ' union all select ' + @Col + '' + 'from ' + @From + ' where ' + @Where
    END
    ELSE IF (@i > 2)
    BEGIN
        SET @Col = @Col + '+'',''' + '+' + CONCAT (
                @Alias
                ,'.' + 'col'
                ,' '
                )
        SET @From = @From + ',' + '#temp ' + @Alias
        SET @Where = @Where + ' and ' + 't' + cast(@i - 1 AS VARCHAR) + '.id < t' + cast(@i AS VARCHAR) + '.id'
        SET @Sql = @Sql + ' union all select ' + @Col + '' + 'from ' + @From + ' where ' + @Where
    END

    SET @i = @i + 1
END

PRINT @Col
PRINT @Sql

EXECUTE sp_executesql @Sql

DROP TABLE #temp

Работаетправильно для N=1,2,3,4.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...