2

studentID我必须找到一对学生,他们从具有和的表中选择完全相同的课程courseID

studentID | courseID
    1           1
    1           2
    1           3
    2           1
    3           1
    3           2
    3           3 

查询应该返回(1, 3)
结果也不应该有重复的行,例如(1,3)(3,1)

4

5 回答 5

9

给定样本数据:

CREATE TABLE student_course (
   student_id integer,
   course_id integer,
   PRIMARY KEY (student_id, course_id)
);

INSERT INTO student_course (student_id, course_id)
VALUES (1, 1), (1, 2), (1, 3), (2, 1), (3, 1), (3, 2), (3, 3) ;

使用数组聚合

一种选择是使用 CTE 加入每个学生正在学习的课程的有序列表:

WITH student_coursearray(student_id, courses) AS (
  SELECT student_id, array_agg(course_id ORDER BY course_id)
  FROM student_course
  GROUP BY student_id
)
SELECT a.student_id, b.student_id
FROM student_coursearray a INNER JOIN student_coursearray b ON (a.courses = b.courses)
WHERE a.student_id > b.student_id;

array_agg实际上是 SQL 标准的一部分,WITH公共表表达式语法也是如此。MySQL 都不支持,因此如果您想支持 MySQL,则必须以不同的方式表达这一点。

查找每个学生缺少的课程配对

另一种思考方式是“对于每个学生配对,找出一个人是否正在上课,而另一个人没有”。这会借给 a FULL OUTER JOIN,但表达起来很尴尬。您必须确定感兴趣的学生 ID 的配对,然后为每个配对在每个班级的集合中进行完全外部连接。如果有任何空行,那么一个会占用一个类,而另一个则没有,因此您可以将其与NOT EXISTS过滤器一起使用以排除此类配对。这给了你这个怪物:

WITH student_id_pairs(left_student, right_student) AS (
  SELECT DISTINCT a.student_id, b.student_id
  FROM student_course a 
  INNER JOIN student_course b ON (a.student_id > b.student_id)
)
SELECT left_student, right_student 
FROM student_id_pairs 
WHERE NOT EXISTS (
  SELECT 1
  FROM (SELECT course_id FROM student_course WHERE student_id = left_student) a
  FULL OUTER JOIN (SELECT course_id FROM student_course b WHERE student_id = right_student) b
    ON (a.course_id = b.course_id)
  WHERE a.course_id IS NULL or b.course_id IS NULL
);

CREATE TEMPORARY TABLE AS SELECT ...CTE 是可选的,如果您的数据库不支持 CTE ,则可以替换为 a或其他任何内容。

使用哪个?

我非常有信心数组方法在所有情况下都会表现得更好,特别是因为对于一个非常大的数据集,您可以使用WITH表达式,从查询中创建一个临时表,在上添加一个索引(courses, student_id)并快速执行平等搜索将很好地真正地支付索引创建时间的成本。您不能使用子查询连接方法来做到这一点。

于 2013-07-29T06:55:25.060 回答
2
select courses,group_concat(studentID) from
(select studentID, 
group_concat(courseID order by courseID) as courses
from Table1 group by studentID) abc
group by courses having courses like('%,%');

小提琴

于 2013-07-29T06:58:36.027 回答
1

天真的关系划分实现,使用 CTE:

WITH pairs AS (
        SELECT DISTINCT a.student_id AS aaa
        , b.student_id AS bbb
        FROM student_course a
        JOIN student_course b ON a.course_id = b.course_id
        )
SELECT *
FROM pairs p
WHERE p.aaa < p.bbb
AND NOT EXISTS (
        SELECT * FROM student_course nx1
        WHERE nx1.student_id = p.aaa
        AND NOT EXISTS (
                SELECT * FROM student_course nx2
                WHERE nx2.student_id = p.bbb
                AND nx2.course_id = nx1.course_id
                )
        )
AND NOT EXISTS (
        SELECT * FROM student_course nx1
        WHERE nx1.student_id = p.bbb
        AND NOT EXISTS (
                SELECT * FROM student_course nx2
                WHERE nx2.student_id = p.aaa
                AND nx2.course_id = nx1.course_id
                )
        )
        ;

同样,没有 CTE:

SELECT *
FROM (
        SELECT DISTINCT a.student_id AS aaa
        , b.student_id AS bbb
        FROM student_course a
        JOIN student_course b ON a.course_id = b.course_id
        ) p
WHERE p.aaa < p.bbb
AND NOT EXISTS (
        SELECT * FROM student_course nx1
        WHERE nx1.student_id = p.aaa
        AND NOT EXISTS (
                SELECT * FROM student_course nx2
                WHERE nx2.student_id = p.bbb
                AND nx2.course_id = nx1.course_id
                )
        )
AND NOT EXISTS (
        SELECT * FROM student_course nx1
        WHERE nx1.student_id = p.bbb
        AND NOT EXISTS (
                SELECT * FROM student_course nx2
                WHERE nx2.student_id = p.aaa
                AND nx2.course_id = nx1.course_id
                )
        )
        ;

显然,非 CTE 版本更快。

于 2013-08-11T14:18:38.097 回答
1

测试用例:

我创建了一个有点现实的测试用例:

CREATE TEMP TABLE student_course (
   student_id integer
  ,course_id integer
  ,PRIMARY KEY (student_id, course_id)
);
INSERT INTO student_course
SELECT *
FROM (VALUES (1, 1), (1, 2), (1, 3), (2, 1), (3, 1), (3, 2), (3, 3)) v
      -- to include some non-random values in test
UNION  ALL
SELECT DISTINCT student_id, normal_rand((random() * 30)::int, 1000, 35)::int
FROM   generate_series(4, 5000) AS student_id;
DELETE FROM student_course WHERE random() > 0.9; -- create some dead tuples
ANALYZE student_course; -- needed for temp table

请注意使用normal_rand()来填充具有正态分布值的虚拟表。它与 tablefunc 模块一起提供,因为无论如何我都要进一步使用它......

还要注意我将要为基准操作以模拟各种测试用例的数字的粗体强调。

纯 SQL

这个问题相当基本且不清楚。找到匹配课程的前两个学生?还是全部找到?找到他们中的情侣或共享相同课程的学生群体?克雷格回答:
找到所有共享相同课程的夫妇。

C1 - Craig 的第一个查询

带有 CTE 并按数组分组的普通 SQL,稍微格式化:

WITH student_coursearray(student_id, courses) AS (
   SELECT student_id, array_agg(course_id ORDER BY course_id)
   FROM   student_course
   GROUP  BY student_id
   )
SELECT a.student_id, b.student_id
FROM   student_coursearray a
JOIN   student_coursearray b ON (a.courses = b.courses)
WHERE  a.student_id < b.student_id
ORDER  BY a.student_id, b.student_id;

克雷格回答中的第二个问题立即退出了比赛。不止几行,性能很快就会严重恶化。是CROSS JOIN毒药。

E1 - 改进版

有一个主要弱点,ORDER BY每个聚合的表现不佳,所以我ORDER BY在子查询中重写了:

WITH cte AS (
   SELECT student_id, array_agg(course_id) AS courses
   FROM  (SELECT student_id, course_id FROM student_course ORDER BY 1, 2) sub
   GROUP  BY student_id
   )
SELECT a.student_id, b.student_id
FROM   cte a
JOIN   cte b USING (courses)
WHERE  a.student_id < b.student_id
ORDER  BY 1,2;

E2 - 问题的替代解释

我认为通常更有用的情况是:
找到所有共享相同课程的学生。
因此,我返回具有匹配课程的学生数组。

WITH s AS (
   SELECT student_id, array_agg(course_id) AS courses
   FROM  (SELECT student_id, course_id FROM student_course ORDER BY 1, 2) sub
   GROUP  BY student_id
   )
SELECT array_agg(student_id)
FROM   s
GROUP  BY courses
HAVING count(*) > 1
ORDER    BY array_agg(student_id);

F1 - 动态 PL/pgSQL 函数

为了使这个通用和快速,我将它包装到一个带有动态 SQL的plpgsql 函数中:

CREATE OR REPLACE FUNCTION f_same_set(_tbl regclass, _id text, _match_id text)
  RETURNS SETOF int[] AS
$func$
BEGIN

RETURN QUERY EXECUTE format(
   $f$
   WITH s AS (
      SELECT %1$I AS id, array_agg(%2$I) AS courses
      FROM   (SELECT %1$I, %2$I FROM %3$s ORDER BY 1, 2) s
      GROUP  BY 1
      )
   SELECT array_agg(id)
   FROM   s
   GROUP  BY courses
   HAVING count(*) > 1
   ORDER    BY array_agg(id)
   $f$
   ,_id, _match_id, _tbl
   );
END
$func$  LANGUAGE plpgsql;

称呼:

SELECT * FROM f_same_set('student_course', 'student_id', 'course_id');

适用于任何具有数字列的表。扩展其他数据类型也很简单。

crosstab()

对于相对较少courses的学生(以及任意数量的学生),crosstab() 附加的tablefunc模块是 PostgreSQL 中的另一个选项。此处提供更多一般信息:
PostgreSQL 交叉表查询

简单案例

问题中简单示例的简单案例,就像链接答案中解释的那样

SELECT array_agg(student_id)
FROM   crosstab('
     SELECT student_id, course_id, TRUE
     FROM   student_course
     ORDER  BY 1'

   ,'VALUES (1),(2),(3)'
   )
AS t(student_id int, c1 bool, c2 bool, c3 bool)
GROUP  BY c1, c2, c3
HAVING count(*) > 1;

F2 - 动态交叉表函数

对于简单的情况,交叉表变体更快,因此我使用动态 SQL 构建了一个 plpgsql 函数并将其包含在测试中。功能与F1相同。

CREATE OR REPLACE FUNCTION f_same_set_x(_tbl regclass, _id text, _match_id text)
  RETURNS SETOF int[] AS
$func$
DECLARE
   _ids int[];   -- for array of match_ids (course_id in example)
BEGIN

-- Get list of match_ids
EXECUTE format(
   'SELECT array_agg(DISTINCT %1$I ORDER BY %1$I) FROM %2$s',_match_id, _tbl)
INTO _ids;

-- Main query
RETURN QUERY EXECUTE format(
   $f$
   SELECT array_agg(%1$I)
   FROM   crosstab('SELECT %1$I, %2$I, TRUE FROM %3$s ORDER BY 1'
                  ,'VALUES (%4$s)')
      AS t(student_id int, c%5$s  bool)
   GROUP  BY c%6$s
   HAVING count(*) > 1
   ORDER    BY array_agg(student_id)
   $f$
   ,_id
   ,_match_id
   ,_tbl
   ,array_to_string(_ids, '),(')     -- values
   ,array_to_string(_ids, ' bool,c') -- column def list
   ,array_to_string(_ids, ',c')      -- names
   );
END
$func$  LANGUAGE plpgsql;

称呼:

SELECT * FROM f_same_set_x('student_course', 'student_id', 'course_id');

基准

我在我的小型 PostgreSQL 测试服务器上进行了测试。Debian Linux 上的 PostgreSQL 9.1.9 和大约 6 年历史的 AMD Opteron 服务器。我使用上述设置和每个呈现的查询运行了 5 个测试集。最好的 5 与EXPLAIN ANALYZE.

我将这些值用于上述测试用例中的粗体数字来填充:

天然橡胶 学生/最大。天然橡胶 课程/标准偏差(导致更不同的 course_ids)
1. 1000 / 30 / 35
2. 5000 / 30 / 50
3. 10000 / 30 / 100
4. 10000 / 10 / 10
5. 10000 / 5 / 5

C1
1. 总运行时间:57 毫秒
2. 总运行时间:315 毫秒
3. 总运行时间:663 毫秒
4. 总运行时间:543 毫秒
5. 总运行时间:2345 毫秒(!) - 多对会恶化

E1
1.总运行时间:46 ms
2.总运行时间:251 ms
3.总运行时间:529 ms
4.总运行时间:338 ms
5.总运行时间:734 ms

E2
1.总运行时间:45 ms
2.总运行时间:245 ms
3.总运行时间:515 ms
4.总运行时间:218 ms
5.总运行时间:143 ms

F1 victor
1.总运行时间:14 ms
2.总运行时间:77 ms
3.总运行时间:166 ms
4.总运行时间:80 ms
5.总运行时间:54 ms

F2
1. 总运行时间:62 毫秒
2. 总运行时间:336 毫秒
3. 总运行时间:1053 毫秒 (!) crosstab() 因许多不同的值而恶化
4. 总运行时间:195 毫秒
5. 总运行时间:105 毫秒 (!)但在不同值较少的情况下表现良好

具有动态 SQL 的 PL/pgSQL 函数,在子查询中对行进行排序是明显的胜利者。

于 2013-07-30T08:14:44.390 回答
0

在 mysql 中完成此操作的过程

Create table student_course_agg 
( 
student_id int,
courses varchar(150)
);

INSERT INTO student_course_agg
select studentID ,GROUP_CONCAT(courseID ORDER BY courseID) courses
FROM STUDENTS 
GROUP BY 1;

SELECT master.student_id m_student_id,child.student_id c_student_id
FROM student_course_agg master 
JOIN student_course_ag child 
    ON master.student_id<child.student_id and master.courses=child.courses;

直接查询。

SELECT master.student_id m_student_id,child.student_id c_student_id
FROM (select studentID ,GROUP_CONCAT(courseID ORDER BY courseID) courses
FROM STUDENTS 
GROUP BY 1) master
JOIN (select studentID ,GROUP_CONCAT(courseID ORDER BY courseID) courses
FROM STUDENTS 
GROUP BY 1) child 
   ON master.studentID <child.studentID and master.courses=child.courses;
于 2014-06-17T02:03:46.263 回答