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_nickname | a.nickname |
c.user_name | c.name |
d.service_duration | d.duration |
b.scheduled_time | b.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。
备注:内容仅供参考。
