You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SQL中建模并更新带顺序的多对多关联?以歌单歌曲场景为例

歌单与歌曲关联关系建模方案(适配PostgreSQL + Supabase场景)

方案1:多对多关联表(生产环境首选,数据一致性高)

表结构设计

-- 歌单表
CREATE TABLE playlists (
  id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  name VARCHAR(255) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 歌曲表
CREATE TABLE songs (
  id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  name VARCHAR(255) NOT NULL,
  artist VARCHAR(255) NOT NULL,
  album VARCHAR(255),
  duration INT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 歌单歌曲关联表,用position字段存储顺序,避免和SQL关键字index重名
CREATE TABLE playlist_songs (
  id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  playlist_id INT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
  song_id INT NOT NULL REFERENCES songs(id) ON DELETE CASCADE,
  position INT NOT NULL,
  -- 保证同一个歌单下不会出现重复的排序位
  UNIQUE(playlist_id, position)
);

-- 加索引加速查询
CREATE INDEX idx_playlist_songs_playlist ON playlist_songs(playlist_id);

顺序更新SQL函数

直接在Supabase的SQL编辑器中执行以下代码创建函数,即可通过SDK直接调用完成顺序更新,不需要逐行修改:

CREATE OR REPLACE FUNCTION update_playlist_order(p_playlist_id INT, p_song_ids INT[])
RETURNS VOID AS $$
BEGIN
  -- 先删除该歌单下原有的关联记录
  DELETE FROM playlist_songs WHERE playlist_id = p_playlist_id;
  -- 批量插入新的关联记录,自动生成排序位
  INSERT INTO playlist_songs (playlist_id, song_id, position)
  SELECT p_playlist_id, song_id, idx
  FROM UNNEST(p_song_ids) WITH ORDINALITY AS t(song_id, idx);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Supabase JS SDK使用示例

// 更新歌单顺序,传入歌单ID和新的歌曲ID数组
const { error } = await supabase.rpc('update_playlist_order', {
  p_playlist_id: 1,
  p_song_ids: [24, 50, 21]
})

// 查询歌单下的所有歌曲,按顺序返回
const { data: playlistSongs } = await supabase
  .from('playlist_songs')
  .select('songs(*)')
  .eq('playlist_id', 1)
  .order('position', { ascending: true })

方案2:歌单表直接存储歌曲ID数组(开发效率优先,适合小项目)

虽然PostgreSQL没有原生支持外键数组,但可以通过触发器实现数据有效性校验,避免出现无效的歌曲ID。

表结构设计

CREATE TABLE playlists (
  id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  name VARCHAR(255) NOT NULL,
  song_ids INT[] NOT NULL DEFAULT '{}', -- 存储有序的歌曲ID数组
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 可选:添加触发器校验song_ids中的ID都存在于songs表中
CREATE OR REPLACE FUNCTION check_song_ids_valid()
RETURNS TRIGGER AS $$
BEGIN
  IF EXISTS (
    SELECT 1 FROM unnest(NEW.song_ids) AS sid
    LEFT JOIN songs ON songs.id = sid
    WHERE songs.id IS NULL
  ) THEN
    RAISE EXCEPTION '歌单中包含不存在的歌曲ID';
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_playlists_check_song_ids
BEFORE INSERT OR UPDATE OF song_ids ON playlists
FOR EACH ROW EXECUTE FUNCTION check_song_ids_valid();

操作示例

// 更新歌单顺序,直接传新的数组即可
const { error } = await supabase
  .from('playlists')
  .update({ song_ids: [24, 50, 21] })
  .eq('id', 1)

// 查询歌单下的所有歌曲,保留数组顺序
const { data: playlist } = await supabase
  .from('playlists')
  .select(`
    id,
    name,
    songs: songs(id, name, artist, album)
  `)
  .eq('id', 1)
// 如需在数据库层面直接关联返回有序结果,可以用视图或RPC函数处理

方案选型建议

  • 关联表方案适合用户量较大、歌单歌曲数量多、频繁对单首歌曲进行增删操作的场景,数据一致性高,查询性能更稳定。
  • 数组方案适合个人项目、小体量应用,开发速度快,顺序更新逻辑简单,维护成本低。

内容的提问来源于stack exchange,提问作者Jacob Haugen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 18:54:00