无状态查询(Stateless Queries)解析及在Supabase Postgres中的应用合理性探讨
场景背景与疑问
我正在使用Supabase Postgres开发,编写了如下两个Postgres函数:
函数1:用户创建与钱包关联
DROP FUNCTION IF EXISTS setup_auth_user; CREATE OR REPLACE FUNCTION setup_auth_user( email_value TEXT, name_value TEXT, password_value TEXT, wallet_address_value TEXT, private_key_value TEXT ) RETURNS user_info AS $$ DECLARE updated_user_info user_info; BEGIN INSERT INTO user_info (user_state, email, name, password, updated_at) VALUES ('active'::user_state, email_value, name_value, password_value, now()) RETURNING * INTO updated_user_info; insert into user_wallet_info (private_key, wallet_address, user_id) VALUES (private_key_value, wallet_address_value, updated_user_info.id); RETURN updated_user_info; END; $$ LANGUAGE plpgsql;
函数2:获取带认证信息的用户
DROP FUNCTION IF EXISTS public.get_user_with_authenticator; CREATE OR REPLACE FUNCTION public.get_user_with_authenticator(session_id_value text) RETURNS jsonb LANGUAGE sql AS $function$ SELECT jsonb_build_object( 'email', u.email, 'name', u.name, 'id', u.id, 'user_state', u.user_state, 'credential_id', pa.credential_id, 'credential_public_key', pa.credential_public_key, 'counter', pa.counter, 'transports', pa.transports, 'challenge', p.passkey_challenge ) FROM passkey_sessions AS p LEFT JOIN user_info AS u ON u.email = p.email LEFT JOIN passkey_authenticator AS pa ON pa.id = u.authenticator_id WHERE p.session_id = session_id_value; $function$;
我在后端调用插入函数,前端直接调用setup_auth_user和get_user_with_authenticator函数。针对这个方案,收到了如下反馈:
使用无状态查询比每次编辑都要创建/更新函数更容易调试,修改查询也不需要迁移。
现需要解答两个问题:
- 什么是无状态查询?
- 在该场景下使用无状态查询是否是个好主意?
解答
1. 什么是无状态查询?
无状态查询指的是直接在应用层(前端或后端)编写并发送原生SQL语句到数据库执行,而不是把查询逻辑封装成Postgres的自定义函数。
举个例子:
- 原本调用
SELECT setup_auth_user('xxx@xx.com', '张三', 'pwd123', '0x...', 'key...'),换成无状态查询就是直接执行:
WITH inserted_user AS ( INSERT INTO user_info (user_state, email, name, password, updated_at) VALUES ('active'::user_state, 'xxx@xx.com', '张三', 'pwd123', now()) RETURNING * ) INSERT INTO user_wallet_info (private_key, wallet_address, user_id) SELECT 'key...', '0x...', id FROM inserted_user RETURNING (SELECT * FROM inserted_user);
- 原本调用
SELECT get_user_with_authenticator('session_123'),换成无状态查询就是直接执行:
SELECT jsonb_build_object( 'email', u.email, 'name', u.name, 'id', u.id, 'user_state', u.user_state, 'credential_id', pa.credential_id, 'credential_public_key', pa.credential_public_key, 'counter', pa.counter, 'transports', pa.transports, 'challenge', p.passkey_challenge ) FROM passkey_sessions AS p LEFT JOIN user_info AS u ON u.email = p.email LEFT JOIN passkey_authenticator AS pa ON pa.id = u.authenticator_id WHERE p.session_id = 'session_123';
这类查询不需要在数据库中预先定义函数,每次执行都是独立的、无持久化逻辑的请求,因此被称为“无状态”。
2. 该场景下使用无状态查询是否是个好主意?
需要结合场景权衡两种方案的优缺点:
无状态查询的优势
- 调试更直接:直接在应用层写SQL,能在本地或数据库客户端(如pgAdmin、Supabase SQL编辑器)直接测试语句,不用先创建/更新函数再调用,出错时能直接定位SQL本身的问题,避免函数内部逻辑的黑盒排查。
- 修改无需迁移:调整查询逻辑(比如加字段、改关联条件)时,直接修改应用层的SQL即可,不用执行
CREATE OR REPLACE FUNCTION这类DDL语句,也不用考虑数据库迁移的问题,迭代速度更快。 - 权限控制更灵活:如果通过Supabase的REST API调用,无状态查询可以配合Row Level Security(RLS)实现更细粒度的访问控制,而函数若定义在public schema,可能需要额外配置权限避免滥用。
使用自定义函数的优势
- 逻辑封装与复用:如果多个地方都需要执行“创建用户+关联钱包”的逻辑,函数可以把这段逻辑封装起来,避免重复编写相同SQL,减少代码冗余。
- 事务安全保障:你的
setup_auth_user函数里的两个INSERT操作处于同一个事务中,要么都成功要么都失败;无状态查询如果在应用层分开执行两次SQL,可能出现中间失败的情况(虽然也可以在应用层开启事务,但需要额外代码)。 - 隐藏敏感逻辑:函数能把复杂业务逻辑(如状态转换、关联规则)隐藏在数据库端,应用层只需要传参调用,不用暴露底层表结构和业务规则,降低数据泄露风险。
- 性能优化潜力:Postgres会缓存函数的执行计划,对于频繁调用的查询,函数可能比每次发送原生SQL有更好的性能(不过差异不大,除非是极复杂的逻辑)。
针对你的场景的建议
- 前端直接调用的场景:不建议用无状态查询直接暴露给前端,因为前端可以随意修改SQL参数甚至语句,容易引发SQL注入风险(即使Supabase支持参数化查询,也不如函数安全)。这种情况下,用函数+RLS控制权限更稳妥。
- 后端调用的场景:可以考虑使用无状态查询,尤其是当逻辑还在快速迭代时,修改起来更方便。但要注意把SQL写成参数化形式避免注入,同时在应用层处理事务(比如用Supabase客户端的事务API)保证数据一致性。
- 逻辑稳定后:可以再将逻辑封装成函数,提升复用性和安全性。
内容的提问来源于stack exchange,提问作者Alwaysblue
相关产品推荐
相关产品推荐

