◆ 記事に対するタグみたいな可変個数のものを一緒に取得したいとき
◆ JOIN は親側のデータの重複でムダが多く 集計処理が必要
◆ クエリ分けるのは無駄は少ないけど手間が多い
◆ 配列・JSON 機能を使って取得すると 1 クエリで求める形で取得できる
  ◆ DB サーバで JSON 化処理の負荷や子側のデータ重複で無駄が出るデメリットはある

記事に対して可変個数のタグがついてるとか そういうデータ構造ってけっこうあります
JSON 風に表現するとこういうのです

const articles = [
{
id: 1,
title: "aa",
tags: [
{
id: 1,
name: "t1",
},
{
id: 2,
name: "tag2",
},
],
},
{
id: 2,
title: "bb",
tags: [
{
id: 1,
name: "t1",
},
{
id: 3,
name: "t3",
},
],
},
]

JSON でデータを扱える DB を使うならそのまま入れればいいのですが RDB だと別々のテーブルに分けて保存するので取得が面倒です

JOIN すれば比較的簡単なクエリを 1 回の実行で済みます
プログラム側ですることは group by のようにまとめるくらいです
ただ タグの数だけ記事の情報が重なって取得されるので無駄が多いです
記事が持つ情報 (列) が多く 記事あたりのタグ数も多いとデータ量がかなり増えてしまいます
特に記事であれば数十 KB 程度の本文がある可能性もあり それがタグ数だけ増えるのは困ります

一般的には別クエリに分ける事が多いのではと思います
一旦記事を取得した上で別のクエリとしてタグ情報を取得します
単純にするなら記事数だけタグ取得のクエリを送ることになりますがこれだとクエリ数が増えて効率が悪いです
工夫するならタグ取得を 1 クエリにまとめられますが 1 つ目のクエリの結果から 2 つ目のクエリの条件を作ってとプログラム側でのやることが増え複雑になります
2 クエリの実行になるので最後に結果を結合する処理もあります

これをお手軽でいい感じに取得できないかなと考えてみたところ 配列や JSON 機能を使えばできそうな気がします

やってみる

上に書いた例のデータを保存するテーブルを作りデータをセットします

create table article (
id serial not null,
title text not null,
constraint article_pkey primary key (id)
);

create table tag (
id serial not null,
name text not null,
constraint tag_pkey primary key (id)
);

create table article_tag (
a_id integer,
t_id integer,
constraint article_tag_pkey primary key (a_id, t_id)
);

insert into article values
(1, 'aa'),
(2, 'bb');

insert into tag values
(1, 't1'),
(2, 't2'),
(3, 't3');

insert into article_tag values
(1, 1),
(1, 2),
(2, 1),
(2, 3);

記事に対して欲しいものが 1 つだけなら配列で簡単にできます
タグ名だけを取得してみます

# select *, array(
# select t.name from article_tag at left join tag t on at.t_id = t.id where at.a_id = a.id
# ) as tags
# from article a;

-[ RECORD 1 ]--
id | 1
title | aa
tags | {t1,t2}
-[ RECORD 2 ]--
id | 2
title | bb
tags | {t1,t3}

サブクエリで記事に対するタグ名を取得するようにしてそれを array 関数に入れます
こうすれば記事のレコードあたり 1 つの値となるので select の 1 項目として受け取れます

ただ 欲しいものがタグ名だけでなく タグ ID とタグ名となるとこの方法は使えません
array() の中は 1 つの列である必要がありますが タグ ID とタグ名だと 2 列になってしまいます

そこで サブクエリの select が 1 列になるように JSON のオブジェクト化を行います
タグ ID とタグ名が 1 つの JSON の値となれば あとは同じように array で配列化できます

# select * , array(
# select json_build_object('id', t.id, 'name', t.name)
# from article_tag at left join tag t on at.t_id = t.id
# where at.a_id = a.id
# ) as tags
# from article a;

-[ RECORD 1 ]----------------------------------------------------------------
id | 1
title | aa
tags | {"{\"id\" : 1, \"name\" : \"t1\"}","{\"id\" : 2, \"name\" : \"t2\"}"}
-[ RECORD 2 ]----------------------------------------------------------------
id | 2
title | bb
tags | {"{\"id\" : 1, \"name\" : \"t1\"}","{\"id\" : 3, \"name\" : \"t3\"}"}

この psql 上の結果を見るとパースが必要そうに見えますが DB ライブラリが結果の型を認識して 適切な型で返してくれるのなら配列や JSON 部分は自分で処理しなくても配列やオブジェクト型になっています
Node.js で knex を使って試してみます

const sql = `
select * , array(
select json_build_object('id', t.id, 'name', t.name)
from article_tag at left join tag t on at.t_id = t.id
where at.a_id = a.id
) as tags
from article a;
`

const pg = require("knex")({ client: "pg", connection: {/* ... */} })
pg.raw(sql).then(result => console.log(result.rows))
[
{
id: 1,
title: 'aa',
tags: [ { id: 1, name: 't1' }, { id: 2, name: 't2' } ]
},
{
id: 2,
title: 'bb',
tags: [ { id: 1, name: 't1' }, { id: 3, name: 't3' } ]
}
]