Rails Rails + Postgresql17 使用 NULLS NOT DISTINCT 还是无法唯一索引允许多个 NULL

chuckaiyu · September 15, 2025 · Last by chuckaiyu replied at September 16, 2025 · 19 hits
create_table :users do |t|
  t.string :email, null: false
  t.string :username, null: false
  t.string :display_name, null: false
  t.string :password_digest, null: false
  t.integer :status, default: 0, null: false
  t.string :security_question
  t.string :security_answer_digest
  t.string :password_reset_token
  t.datetime :password_reset_sent_at
  t.string :auth_token

  t.timestamps
end

add_index :users, :email, unique: true
add_index :users, :username, unique: true
add_index :users, :auth_token, unique: true, nulls_not_distinct: true
add_index :users, :password_reset_token, unique: true, nulls_not_distinct: true

测试一直跑不过,看了日志,数据库报错 duplicate key value。然后,直接在 Pg 里面去试试。

-- 1. 创建测试表
DROP TABLE IF EXISTS test_nulls_distinct;
CREATE TABLE test_nulls_distinct (
    id SERIAL PRIMARY KEY,
    data TEXT
);

-- 2. 创建带 NULLS NOT DISTINCT 的唯一索引
CREATE UNIQUE INDEX idx_test_data_unique
ON test_nulls_distinct (data)
NULLS NOT DISTINCT;

-- 3. 插入测试数据
-- 插入唯一非 NULL  (应该成功)
INSERT INTO test_nulls_distinct (data) VALUES ('A');
INSERT INTO test_nulls_distinct (data) VALUES ('B');

-- 插入多个 NULL  (这应该成功因为 NULLS NOT DISTINCT)
INSERT INTO test_nulls_distinct (data) VALUES (NULL);
INSERT INTO test_nulls_distinct (data) VALUES (NULL);

-- 执行上面这行应该会报错:
-- ERROR:  duplicate key value violates unique constraint "idx_test_data_unique"
-- DETAIL:  Key (data)=(NULL) already exists.

有没有同学解决过这个问题,网上搜索了半天资料,都是说会遇到错误都是 Pg 版本低,我一开始用 16,最后换到 latest,还是一样。

我查文档 UNIQUE NULLS NOT DISTINCT 是指 null 视为同一个值,也就不符合 unique.

https://www.postgresql.org/about/featurematrix/detail/unique-nulls-not-distinct/

不加 NULLS NOT DISTINCT 就符合你需要的插入多个 null。


题外话:我一般把 auth_token 设为 null: false,逻辑更简单。

问了 ChatGPT, 它说:

在 PostgreSQL 里,默认情况下唯一索引会允许多个 NULL, 但如果你指定了 NULLS NOT DISTINCT,就会把 NULL 当作相等值处理,从而不允许多个 NULL 出现。

所以你可以试试去掉 nulls_not_distinct: true. 这 nulls_not_distinct 其实说得也很直白,就是把多个 nulls 认定为非 distinct, 插入第二个 null 就破坏了 distinct.

我没花时间尝试,这种情况我一般像 @Rei 那样找个大众一点的方式,这样才不会出小众的错。

谢谢 @Rei @Peter

从头又检查了一遍,换了一个思路。可能不是数据库那个重复 null 报错。把 password_reset_token 改成 reset_password_token,估计是和那里冲突了。测试用例检查 token 是否生成也通过了。

You need to Sign in before reply, if you don't have an account, please Sign up first.