テストケース:
やや現実的なテスト ケースを作成しました。
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
質問はかなり基本的で不明確です。一致するコースを持つ最初の 2 人の学生を見つけますか? それとも全部見つけますか?それらのカップルまたは同じコースを共有する学生のグループを見つけますか? Craig の回答:同じコースを共有しているすべてのカップル
を見つけます。
単純な SQL 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
JOIN student_coursearray b ON (a.courses = b.courses)
WHERE a.student_id < b.student_id
ORDER BY a.student_id, b.student_id;
Craig の回答の2 番目のクエリは、すぐにレースから脱落しました。数行以上になると、パフォーマンスが急速に低下します。CROSS JOIN
毒です。
E1 - 改良版
大きな弱点が 1 つあります。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()
追加のtablefuncモジュール によって提供される比較的少数のcourses
(そして任意に多数の) 学生に対しては、PostgreSQL の別のオプションです。ここでより一般的な情報: PostgreSQL クロス集計クエリcrosstab()
シンプルなケース
リンクされた回答で説明されているのと同じように、質問の簡単な例の簡単なケース:
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 テスト サーバーでテストしました。〜 6 年前の AMD Opteron サーバー上の Debian Linux 上の PostgreSQL 9.1.9。上記の設定と提示された各クエリを使用して、5 つのテスト セットを実行しました。ベスト オブ 5 とEXPLAIN ANALYZE
.
上記のテストケースの太字の数字にこれらの値を使用して入力しました。
番号 生徒数/最大 番号 コース数 / 標準偏差 (より明確な course_id の結果)
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 ミリ秒
2. 合計実行時間: 251 ミリ秒
3. 合計実行時間: 529 ミリ秒
4. 合計実行時間: 338 ミリ秒
5. 合計実行時間: 734 ミリ秒
E2
1. 合計実行時間: 45 ミリ秒
2. 合計実行時間: 245 ミリ秒
3. 合計実行時間: 515 ミリ秒
4. 合計実行時間: 218 ミリ秒
5. 合計実行時間: 143 ミリ秒
F1 勝利者
1. 合計実行時間: 14 ミリ秒
2. 合計実行時間: 77 ミリ秒
3. 合計実行時間: 166 ミリ秒
4. 合計実行時間: 80 ミリ秒
5. 合計実行時間: 54 ミリ秒
F2
1. 合計実行時間: 62 ミリ秒
2. 合計実行時間: 336 ミリ秒
3. 合計実行時間: 1053 ミリ秒 (!) crosstab() は多くの異なる値で劣化します
4. 合計実行時間: 195 ミリ秒
5. 合計実行時間: 105 ミリ秒 (!)しかし、より少ない個別の値でうまく機能します
動的 SQL を使用した PL/pgSQL 関数、サブクエリ内の行の並べ替えは明らかに勝利者です。