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

3

我是一个有用的助手,能够翻译文本。

我有一个棘手的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个组合。
我可以想象它是一种交叉连接。我尝试过各种解决方案,使用窗口函数和交叉应用程序等,但它们都有某种缺陷。它们往往会因为每个学生的课程数量和每个课程的时间块数量不同而被绊倒。
感谢您提供的任何帮助!如果需要,我可以粘贴我所拥有的查询的混乱代码!
Alex

课程、课堂和资格证书是同一个概念吗? - Beth
是的,courseBlock表存在。这就是我从中生成上述列表的内容。在此上下文中,“Course”、“Lesson”和“Qualification”是相同的。 - EvalKenEval
1
你使用哪种数据库管理系统?Postgres?Oracle? - user330315
@Alex,你能否在问题中添加创建表语句和插入语句? - Lennart - Slava Ukraini
@Leonard- 我明天会把东西拼凑起来。 - EvalKenEval
显示剩余8条评论
5个回答

5
对于固定数量的资格证书,答案相对简单 - 先前答案中的CROSS JOIN选项将完美地起作用。
然而,如果资格证书数量未知或可能在未来发生变化,则硬编码四个CROSS JOIN操作将不起作用。在这种情况下,答案变得更加复杂。
对于较少量的行,可以使用DBA上此答案的变体,该答案使用二的幂和位比较生成组合。但是,这将受到非常少量行的限制。
对于较大量的行,可以使用函数从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;
}

(基于此函数。)

一旦函数就位,生成有效组合列表就相当容易:

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
;

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

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

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

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

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没有堆栈或数组,因此我不得不使用表变量来模拟它们。


这看起来非常不错,Richard!我现在会尝试实施它。 - EvalKenEval
太棒了,伙计!这是一个赢家。 - EvalKenEval

0

一个巨大的交叉连接?

select * from tablea,tableb,tablec,tabled

这将实际上适用于您所需的内容,其中tablea是生物条目,b是化学,c是计算机和d是教程。您可以更好地指定连接:

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

从技术上讲,这两个语句是相同的...这都是交叉连接,因此上面的逗号版本更简单,在更复杂的查询中,您将希望使用第二个语句,以便您可以非常明确地指出您在进行交叉连接还是内部/左连接。

您可以使用选择联合语句替换'table'条目,以以查询形式提供您要查找的值:

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)


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%'

0

让我们使用这样的范例,其中Table1是生物学,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语句可能会因使用的数据库而有所不同。例如,!= 可以写作 <>。

希望这可以帮到你!


0

我并没有看到问题,但是这个sqlFiddle能用吗?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接