不能直接拼接表名或列名,因为会导致SQL注入;必须用quote_ident()或format('%I', ...)安全转义每个标识符,并提前校验非空与合法性。

为什么不能直接拼接表名或列名?
PostgreSQL 不允许把用户输入直接插进 SQL 字符串里当标识符用,比如 SELECT * FROM ' + user_input + '。哪怕输入是 users,一旦变成 users; DROP TABLE accounts; 就会执行注入语句。SQL 注入在标识符位置同样危险,而 quote_ident() 是唯一被设计来防这种问题的内置函数。
quote_ident() 的正确用法和限制
quote_ident() 只处理单个标识符(如一个表名、一个列名),不支持路径式名称(如 schema.table)或带引号的复合名。它会自动加双引号,并转义内部的双引号(变成两个双引号),同时把空字符串、NULL 返回为 "" —— 这意味着你必须提前校验输入是否为空或非法。
- 合法输入:
quote_ident('user_id')→"user_id" - 含特殊字符:
quote_ident('order by')→"order by" - 含双引号:
quote_ident('name"full')→"name""full" - 危险输入:
quote_ident('users; DROP TABLE x')→"users; DROP TABLE x"(只是字面量,不会执行) - 注意:
quote_ident(NULL)返回"",不是报错,容易导致静默错误
动态 SQL 中怎么组合多个标识符?
不能对整个 SQL 字符串调用 quote_ident(),只能对每个独立标识符分别处理,再用字符串拼接。例如构建 SELECT col1, col2 FROM schema.table WHERE id = $1 时,col1、col2、schema、table 都要各自过一遍 quote_ident(),再用 || 拼起来。
常见错误写法:'SELECT * FROM ' || quote_ident(user_input) —— 看似安全,但如果 user_input 是 public.users,结果就是 "public.users",PostgreSQL 会把它当成一个名字含点的表,而不是 schema.table。
- 正确拆分:
quote_ident(schema_name) || '.' || quote_ident(table_name) - 别忘了检查
schema_name是否为空或只含空白符,否则拼出".users"会报错 - 如果涉及排序字段(如
ORDER BY ?),必须确保传入的是单个列名,不能是col1 ASC, col2 DESC这种表达式 ——quote_ident()不处理排序方向
在 PL/pgSQL 函数里使用 quote_ident 的典型陷阱
最容易被忽略的是:在 EXECUTE 里拼接 SQL 时,quote_ident() 必须在 EXECUTE 内部调用,不能在外部拼好再传进去。因为 EXECUTE 的参数绑定(USING)只适用于值($1),不适用于标识符。
DO $$
BEGIN
-- ❌ 错误:quote_ident 在 EXECUTE 外执行,但变量未被识别
EXECUTE 'SELECT * FROM ' || quote_ident('users');
<p>-- ✅ 正确:quote_ident 在 EXECUTE 字符串内调用
EXECUTE format('SELECT * FROM %I', 'users');
END $$;</p>
format('%I', ...) 是更推荐的方式,它底层就调用 quote_ident(),且支持批量替换、可读性更好;%I 专用于标识符,%L 用于字面量值。但要注意:如果传入的是变量,仍需确保变量本身不为空或非法 —— format() 不做业务逻辑校验。
真正难的不是调用 quote_ident(),而是判断哪些字符串属于“标识符”范畴:别把用户想查的关键词(如搜索词)也喂给它,也别把已经加过引号的字符串再套一层 —— 那会生成 """col""" 这种非法语法。










