SQL: filter data berdasarkan beberapa baris data

Post kali ini, saya ingin membahas contoh kasus yang ditanyakan di stackoverflow.

Dimana pertanyaan inti-nya adalah, bagaimana menampilkan data post yang memiliki relasi terhadap tag dalam table relasi, namun yang diinginkan adalah melakukan filter post yang memiliki beberapa tag, bukan hanya tag tertentu saja.

Misalnya,

  1. post “Hallo Dunia” memiliki tag “tag1”
  2. post “Apa Kabar” memiliki tag “tag2”
  3. post “Pilih Saya” memiliki tag “tag1” DAN “tag2”

Nah, yang diinginkan adalah menampilkan post yang memiliki 2 tag “tag1” dan “tag2”. Bukan salah satu tag saja, di contoh di atas, akan muncul post “Pilih Saya”.

Saya mendesain ulang contoh tabel yang digunakan seperti di bawah ini.

Table yang digunakan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
konglie=# \d
             List OF relations
 Schema |      Name       | TYPE  |  Owner  
--------+-----------------+-------+---------
 public | ppost           | TABLE | konglie
 public | ptags           | TABLE | konglie
 public | rel_ppost_ptags | TABLE | konglie
(3 ROWS)
 
konglie=# \d ppost 
            TABLE "public.ppost"
 COLUMN |         TYPE          | Modifiers 
--------+-----------------------+-----------
 pid    | INTEGER               | 
 pname  | CHARACTER VARYING(20) | 
 
konglie=# \d ptags 
            TABLE "public.ptags"
 COLUMN |         TYPE          | Modifiers 
--------+-----------------------+-----------
 tid    | INTEGER               | 
 tname  | CHARACTER VARYING(20) | 
 
konglie=# \d rel_ppost_ptags 
TABLE "public.rel_ppost_ptags"
 COLUMN |  TYPE   | Modifiers 
--------+---------+-----------
 pid    | INTEGER | 
 tid    | INTEGER |

Sample Data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
konglie=# SELECT * FROM ppost ;
 pid | pname  
-----+--------
 100 | post 1
 101 | post 2
 102 | post 3
(3 ROWS)
 
konglie=# SELECT * FROM ptags ;
 tid  | tname 
------+-------
 1000 | tag1
 1001 | tag2
 1002 | tag3
 1004 | tag5
 1005 | tag6
 1003 | tag4
(6 ROWS)
 
konglie=# SELECT * FROM rel_ppost_ptags ;
 pid | tid  
-----+------
 100 | 1000    *
 100 | 1001    *
 100 | 1003    *
 101 | 1000    **
 101 | 1001    **
 102 | 1001
 102 | 1002
 102 | 1001
 101 | 1001
 101 | 1003    **
 102 | 1001
(11 ROWS)

Data yang diinginkan adalah bagaimana mengambil data atau post, yang memiliki beberapa tag tertentu. Misalnya, tampilkan data post yang memiliki tag tag1,tag2, dan tag4. Data yang hanya memiliki salah satu tag tersebut tidak ikut ditampilkan. Hanya post yang memiliki ketiga tag tersebut saja, yang akan ditampilkan.

Dalam contoh ini, post yang diinginkan adalah post dengan pid 100 dan 101, perhatikan yang diberi tanda * dan ** di atas.

Solusi yang terpikirkan adalah sebagai berikut.

1. Tentukan tid dari tag yang diinginkan, dalam contoh ini, adalah mengambil tid dari table ptags berdasarkan tname.

1
2
3
4
5
6
7
konglie=# SELECT tid FROM ptags WHERE tname IN ('tag1', 'tag2', 'tag4');
 tid  
------
 1000
 1001
 1003
(3 ROWS)

2. Kumpulkan terlebih dahulu seluruh post yang memiliki tag tag yang diinginkan. Sampai tahap ini, post dengan tag yang tidak lengkap pun akan dimunculkan.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
konglie=# SELECT pid, tid FROM rel_ppost_ptags WHERE tid IN ( SELECT tid FROM ptags WHERE tname IN ('tag1', 'tag2', 'tag4' ) );
 pid | tid  
-----+------
 100 | 1000
 100 | 1001
 100 | 1003
 101 | 1000
 101 | 1001
 102 | 1001     *
 102 | 1001     *
 101 | 1001
 101 | 1003
 102 | 1001     *
(10 ROWS)

Perhatikan baris diberi tanda *, yang merupakan data-data yang terduplikasi. Sebenarnya data ini hanya memberi contoh saja, dalam kenyataannya, seharusnya hal ini tidak akan terjadi, dan table rel_ppost_ptags akan lebih baik jika memberikan batasan UNIQUE atau PRIMARY KEY pada 2 field, pid dan tid. Sehingga tidak akan ada data yang menduplikasi pid dan tid secara bersamaan.

Untuk itu, akan kita tambahkan keyword distinct

1
2
3
4
5
6
7
8
9
10
11
konglie=# SELECT DISTINCT pid, tid FROM rel_ppost_ptags WHERE tid IN ( SELECT tid FROM ptags WHERE tname IN ('tag1', 'tag2', 'tag4' ) );
 pid | tid  
-----+------
 100 | 1001
 100 | 1003
 102 | 1001
 101 | 1000
 100 | 1000
 101 | 1001
 101 | 1003
(7 ROWS)

Dengan distinct, baris baris yang terduplikasi akan dihilangkan.

3. Hitung jumlah data berdasarkan pid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
konglie=# SELECT
        pid, COUNT(*)
    FROM (
        SELECT DISTINCT pid, tid FROM rel_ppost_ptags WHERE tid IN ( 
            SELECT tid FROM ptags WHERE tname IN ('tag1', 'tag2', 'tag4' ) 
        )                                             
    ) tmp
    GROUP BY pid;
 
 pid | COUNT 
-----+-------
 101 |     3
 100 |     3
 102 |     1
(3 ROWS)

4. Pada langkah kedua, kita sudah menampilkan data pid dan tid sesuai dengan tag yang kita inginkan, hanya saja belum menentukan apakah sebuah pid memiliki semua kriteria tag yang kita inginkan. Namun, kita dapat pastikan bahwa, apabila pid tersebut memiliki semua tag yang kita inginkan, maka jumlah pid dari hasil sql tersebut akan ada sebanyak n buah, dimana n adalah jumlah tag yang kita inginkan, dalam contoh ini adalah 3. Nah, pada langkah di atas, kita sudah menghitung jumlah pid yang ada, langkah terakhir ini hanya memfilter semuah pid yang berjumlah n tadi saja, dengan memberikan keyword having.

konglie=# select
        pid, count(*)
    from (
        select distinct pid, tid from rel_ppost_ptags where tid in ( 
            select tid from ptags where tname in ('tag1', 'tag2', 'tag4' ) 
        )                                             
    ) tmp
    group by pid having count(*) = 3;

 pid | count 
-----+-------
 101 |     3
 100 |     3
(2 rows)

Dengan demikian, kita sudah dapat memfilter post yang memiliki tag-tag tertentu secara keseluruhan, bukan hanya memiliki salah satu tag saja.

Sebagai tambahan informasi, sql di atas diujicobakan menggunakan Database PostgreSQL 9.4, namun saya rasa tidak ada fungsi spesific database di atas sehingga besar kemungkinan akan dapat digunakan di database server lainnya.

Semoga bermanfaat.