使用SQL查询生成时间表的所有可能组合

时间:2014-08-20 16:34:29

标签: sql sql-server sql-server-2012

我有一个尴尬的SQL拼图,它击败了我。

我正在尝试生成学生单元的可能配置列表,以便我可以将他们的课程选择纳入时间表。学生可能的资格和学习列表如下:

Biology A
Biology C 
Biology D 
Biology E 

Chemistry B 
Chemistry C 
Chemistry D 
Chemistry E 
Chemistry F 

Computing D
Computing F 

Tutorial A 
Tutorial B 
Tutorial E 

学生可能采用的积木解决方案

Biology D
Chemistry C 
Computing F 
Tutorial E 

如何查询上述数据集,为学生提供课程和积分的所有可能组合?然后,我可以削减列表,删除那些冲突并选择一个有效的列表。我估计在这种情况下总共会有大约120种组合。

我可以想象它会是某种交叉连接。我已经尝试了使用窗口函数和交叉应用等各种解决方案,但它们都有一些缺陷。他们都往往被绊倒,因为每个学生都有不同数量的课程,每门课程都有不同数量的课程。

欢呼您提供任何帮助!如果有必要的话,我可以粘贴在我查询的粗糙的混乱中!

亚历

5 个答案:

答案 0 :(得分:5)

对于固定数量的资格,答案相对简单 - 以前答案中的CROSS JOIN选项将完美运作。

但是,如果资格数量未知,或将来可能会发生变化,则硬编码四个CROSS JOIN操作将无效。在这种情况下,答案会变得更复杂。

对于少量行,您可以使用this answer on DBA的变体,它使用2的幂和比特比较来生成组合。但是,这将限制为非常少的行。

对于较大数量的行,您可以使用函数从“N”行生成“M”数字的每个组合。然后,您可以将其加回到源数据计算的ROW_NUMBER值以获取原始行。

生成组合的函数可以用TSQL编写,但如果可能的话,使用SQLCLR会更有意义:

[SqlFunction(
    DataAccess = DataAccessKind.None,
    SystemDataAccess = SystemDataAccessKind.None,
    IsDeterministic = true,
    IsPrecise = true,
    FillRowMethodName = "FillRow",
    TableDefinition = "CombinationId bigint, Value int"
)]
public static IEnumerable Combinations(SqlInt32 TotalCount, SqlInt32 ItemsToPick)
{
    if (TotalCount.IsNull || ItemsToPick.IsNull) yield break;

    int totalCount = TotalCount.Value;
    int itemsToPick = ItemsToPick.Value;
    if (0 >= totalCount || 0 >= itemsToPick) yield break;

    long combinationId = 1;
    var result = new int[itemsToPick];
    var stack = new Stack<int>();
    stack.Push(0);

    while (stack.Count > 0)
    {
        int index = stack.Count - 1;
        int value = stack.Pop();

        while (value < totalCount)
        {
            result[index++] = value++;
            stack.Push(value);

            if (index == itemsToPick)
            {
                for (int i = 0; i < result.Length; i++)
                {
                    yield return new KeyValuePair<long, int>(
                        combinationId, result[i]);
                }

                combinationId++;
                break;
            }
        }
    }
}

public static void FillRow(object row, out long CombinationId, out int Value)
{
    var pair = (KeyValuePair<long, int>)row;
    CombinationId = pair.Key;
    Value = pair.Value;
}

(基于this function。)

一旦功能到位,生成有效组合列表相当容易:

DECLARE @Blocks TABLE 
(
    Qualification varchar(10) NOT NULL, 
    Block char(1) NOT NULL, 
    UNIQUE (Qualification, Block)
);

INSERT INTO @Blocks 
VALUES
    ('Biology', 'A'),
    ('Biology', 'C'), 
    ('Biology', 'D'), 
    ('Biology', 'E'),
    ('Chemistry', 'B'), 
    ('Chemistry', 'C'), 
    ('Chemistry', 'D'), 
    ('Chemistry', 'E'), 
    ('Chemistry', 'F'), 
    ('Computing', 'D'),
    ('Computing', 'F'), 
    ('Tutorial', 'A'), 
    ('Tutorial', 'B'), 
    ('Tutorial', 'E') 
;

DECLARE @Count int, @QualificationCount int;

SELECT
    @Count = Count(1),
    @QualificationCount = Count(DISTINCT Qualification)
FROM
    @Blocks
;

WITH cteNumberedBlocks As
(
    SELECT
        ROW_NUMBER() OVER (ORDER BY Qualification, Block) - 1 As RowNumber,
        Qualification,
        Block
    FROM
        @Blocks
),
cteAllCombinations As
(
    SELECT
        C.CombinationId,
        B.Qualification,
        B.Block
    FROM
        dbo.Combinations(@Count, @QualificationCount) As C
        INNER JOIN cteNumberedBlocks As B
        ON B.RowNumber = C.Value
),
cteMatchingCombinations As
(
    SELECT
        CombinationId
    FROM
        cteAllCombinations
    GROUP BY
        CombinationId
    HAVING
        Count(DISTINCT Qualification) = @QualificationCount
    And
        Count(DISTINCT Block) = @QualificationCount
)
SELECT
    DENSE_RANK() OVER(ORDER BY C.CombinationId) As CombinationNumber,
    C.Qualification,
    C.Block
FROM
    cteAllCombinations As C
    INNER JOIN cteMatchingCombinations As MC
    ON MC.CombinationId = C.CombinationId
ORDER BY
    CombinationNumber,
    Qualification
;

此查询将生成一个包含43个有效组合的172行列表:

1  Biology    A
1  Chemistry  B
1  Computing  D
1  Tutorial   E

2  Biology    A
2  Chemistry  B
2  Computing  F
2  Tutorial   E
...

如果您需要Combinations函数的TSQL版本:

CREATE FUNCTION dbo.Combinations
(
    @TotalCount int,
    @ItemsToPick int
)
Returns @Result TABLE
(
    CombinationId bigint NOT NULL,
    ItemNumber int NOT NULL,
    Unique (CombinationId, ItemNumber)
)
As
BEGIN
DECLARE @CombinationId bigint;
DECLARE @StackPointer int, @Index int, @Value int;
DECLARE @Stack TABLE 
( 
    ID int NOT NULL Primary Key,
    Value int NOT NULL
);
DECLARE @Temp TABLE
(
    ID int NOT NULL Primary Key,
    Value int NOT NULL Unique
);

    SET @CombinationId = 1;

    SET @StackPointer = 1;
    INSERT INTO @Stack (ID, Value) VALUES (1, 0);

    WHILE @StackPointer > 0
    BEGIN
        SET @Index = @StackPointer - 1;
        DELETE FROM @Temp WHERE ID >= @Index;

        -- Pop:
        SELECT @Value = Value FROM @Stack WHERE ID = @StackPointer;
        DELETE FROM @Stack WHERE ID = @StackPointer;
        SET @StackPointer -= 1;

        WHILE @Value < @TotalCount
        BEGIN
            INSERT INTO @Temp (ID, Value) VALUES (@Index, @Value);
            SET @Index += 1;
            SET @Value += 1;

            -- Push:
            SET @StackPointer += 1;
            INSERT INTO @Stack (ID, Value) VALUES (@StackPointer, @Value);

            If @Index = @ItemsToPick
            BEGIN
                INSERT INTO @Result (CombinationId, ItemNumber)
                SELECT @CombinationId, Value
                FROM @Temp;

                SET @CombinationId += 1;
                SET @Value = @TotalCount;
            END;
        END;
    END;

    Return;
END

它与SQLCLR版本几乎相同,只是TSQL没有堆栈或数组,所以我不得不用表变量伪造它们。

答案 1 :(得分:0)

一个巨大的交叉加入?

select * from tablea,tableb,tablec,tabled

这实际上适用于你需要的东西,其中tablea是生物学条目,b是chem,c是计算,d是教程。您可以更好地指定连接:

select * from tablea cross join tableb cross join tablec cross join tabled.

从技术上讲,两个语句都是相同的...这是所有交叉连接,所以上面的逗号版本更简单,在更复杂的查询中,您将要使用第二个语句,这样您就可以非常明确地指向哪里你是交叉加入vs内/左连接。

您可以替换&#39;表格&#39;具有select union语句的条目,用于以查询形式提供您要查找的值:

select * from  
(select 'biology' as 'course','a' as 'class' union all  select 'biology','c'  union all select 'biology','d' union all select 'biology','e') a cross join
(select 'Chemistry' as 'course','b' as 'class' union all  select 'Chemistry','c'  union all select 'Chemistry','d' union all select 'Chemistry','e' union all select 'Chemistry','f') b cross join
(select 'Computing' as 'course','a' as 'class' union all  select 'Computing','c') c cross join
(select 'Tutorial ' as 'course','a' as 'class' union all  select 'Tutorial ','b'  union all select 'Tutorial ','e') d

有120个结果(4 * 5 * 3 * 2)

答案 2 :(得分:0)

没有真正看到问题,但此sqlFiddle是否有效?

答案 3 :(得分:0)

你应该可以使用一个简单的联合,但是每个联合选择只有一个类类型的过滤器,所以你不能得到 BIO,BIO,BIO,BIO,BIO BIO,CHEM,BIO,BIO,BIO 等...

select
      b.course as BioCourse,
      c.course as ChemCourse,
      co.course as CompCourse,
      t.course as Tutorial
  from
      YourTable b,
      YourTable c,
      YourTable co,
      YourTable t
  where
          b.course like 'Biology%'
      AND c.course like 'Chemistry%'
      AND co.course like 'Computing%'
      AND t.course like 'Tutorial%'

答案 4 :(得分:0)

让我们使用Table1是Biology的范例,Table2是化学,Table3是计算,Table4是教程。每个表都有1列,这是该表或课程的可能块。为了获得所有可能的组合,我们希望将所有表格中的笛卡尔积相合,然后过滤掉具有重复字母的行。

最终结果中的每一列将代表其各自的课程。这意味着完成的表中的第1列将是生物学的块字母,即Table1。

所以答案的SQL看起来像这样。

SELECT * FROM Table1,Table2,Table3,Table4 
WHERE col1 != col2
AND col1 != col3
AND col1 != col4
AND col2 != col3
AND col2 != col4
AND col3 != col4;

注意:这很简单,可以扩展到每个表有2列的情况,第一列是主题,第二列是块。替换只需要在where子句中完成,但如果我忽略这种情况,代码就更容易理解。

这有点冗长,但如果每个学生必须拥有每个表中的一个类,并且最多类的数量是4个类,则此方法有效。如果学生不需要4个班级,这个解决方案就会崩溃。

所需的确切SQL可能会有所不同,具体取决于您使用的数据库。例如!=可能是&lt;&gt;。

希望这有帮助!