PAYATHON 2026

PostgreSQL 查询预约时为何会把同一预约关联到多个用户?

数据阿明

原因是用户关联条件不完整

当前查询只通过公司关联用户:

join users_tb c on a.id = c.company_id

这个条件会把每条预约与同公司的所有用户匹配。John Doe 和 Ian F 的 company_id 都是 1,所以同一条预约会分别出现在两个人名下。

如果 schedules_tb.user_id 指向预约对应的 users_tb.id,应改成:

join users_tb c
  on c.id = b.user_id
 and c.company_id = b.company_id

这项修改需要应用到原函数的两个分支。只在 ELSE 分支增加用户名筛选,不能补上预约与用户之间缺失的关联条件。

先确认 user_id 和 customer_id 的含义

示例中所有预约的 user_id 都是 1,而 users_tb.id = 1 对应 John Doe。如果预约确实通过 user_id 关联该用户,这些预约都应显示 John Doe。

输入没有提供外键定义,因此无法确定 user_id 表示预约所属用户、服务人员还是创建人,也无法确定 customer_id 指向哪张表。如果要查询客户姓名,必须沿实际的客户关联关系查询,不能仅凭字段名把 user_id 换成 customer_id。

下面的代码以 schedules_tb.user_id → users_tb.id 为前提。

按展示的字段名重写函数

原函数使用的字段名与表格展示的有几处不同:

原函数使用的字段表格展示的字段
a.company_nicknamea.nickname
c.user_namec.name
d.service_durationd.duration
b.scheduled_timeb.schedule

下面使用表格展示的字段名,执行前应与实际数据库结构核对。示例假定 schedule 保存预约时间,duration 可以转换为整数。

CREATE OR REPLACE FUNCTION get_appointment(
    customerName varchar,
    companyNickname varchar
)
RETURNS TABLE (
    name varchar,
    duration int,
    scheduled timestamp
)
LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN QUERY
    SELECT
        c.name::varchar,
        d.duration::int,
        b.schedule::timestamp
    FROM company_tb AS a
    JOIN schedules_tb AS b
      ON b.company_id = a.id
    JOIN users_tb AS c
      ON c.id = b.user_id
     AND c.company_id = b.company_id
    JOIN services_tb AS d
      ON d.id = b.service_id
     AND d.company_id = b.company_id
    WHERE a.nickname = companyNickname
      AND b.schedule > CURRENT_TIMESTAMP
      AND (
          customerName IS NULL
          OR TRIM(customerName) = ''
          OR c.name = customerName
      );
END;
$function$;

这里把两个分支合并为一次查询:用户名为 NULL、空字符串或全为空格时,不限制姓名;其他情况将传入值与 c.name 比较。

服务也直接通过预约的 service_id 关联,并保留公司一致的条件。原查询已经在 WHERE 中使用了 b.service_id = d.id,因此本次错误关联的关键在用户 JOIN。

原始代码中的函数体定界符和结束语句也需要修正:单独的 $ 不是有效的 dollar quoting 定界符,应使用 $$ 或 $function$,函数体末尾应写成 END;。

用预约 ID 检查匹配关系

排查时可以先查询预约 ID、预约中的用户 ID 和匹配到的用户,确认每条预约关联了谁:

SELECT
    b.id AS appointment_id,
    b.user_id AS appointment_user_id,
    c.id AS matched_user_id,
    c.name,
    b.schedule
FROM schedules_tb AS b
JOIN users_tb AS c
  ON c.id = b.user_id
 AND c.company_id = b.company_id
WHERE b.company_id = 1
ORDER BY b.id;

这条查询暂时不加入时间和服务筛选,方便单独检查用户关联。若 users_tb.id 是唯一键,按照所给数据,每条预约都应匹配到 John Doe。

不要用 DISTINCT 修复这个问题。带有 John Doe 和 Ian F 两个不同姓名的结果行并不完全相同,仍需修正关联关系。

核对影响结果的其他条件

示例调用传入的是 'agendaai-barber',但公司表展示的 nickname 是 'agendaai'。如果采用上面的字段和数据,应这样调用:

-- 查询该公司的全部未来预约
SELECT * FROM get_appointment('', 'agendaai');

-- 查询指定用户的未来预约
SELECT * FROM get_appointment('John Doe', 'agendaai');

此外,还有三处会影响结果:

  • b.schedule > CURRENT_TIMESTAMP 只保留执行查询时尚未到期的预约。如果示例中的时间已经过去,返回空结果符合这个条件。
  • 预约 id = 2 使用了 service_id = 3,而提供的服务数据只有 id = 1 和 id = 2。如果实际数据也如此,该预约会被服务表的 INNER JOIN 排除。若需要保留服务记录缺失的预约,应根据业务要求使用 LEFT JOIN,并接受服务时长可能为 NULL。
  • 最初展示的结果时长为 50,而服务表中对应 id = 1 的时长是 60。这些样例并不完全一致,因此不能据此承诺修正后的精确结果行数或时长。

修正后仍然可能有多条 John Doe 的记录,因为示例中有多条不同的预约都关联 user_id = 1。判断是否重复时,应同时查看预约 ID。

备注:内容仅供参考。