elitan/postgres-nanoid
GitHub: elitan/postgres-nanoid
一个 PostgreSQL 扩展函数,在数据库层直接生成带前缀、URL 友好的安全唯一标识符。
Stars: 68 | Forks: 0
# PostgreSQL Nanoid
为 PostgreSQL 提供安全、URL 友好的唯一标识符。简单、快速、随处可用。
## 安装
**适用于所有 Postgres 提供商:**
- AWS RDS, Google Cloud SQL, Azure Database 等
- 自托管的 Postgres (v12+)
- 需要 `pgcrypto` 扩展(大多数托管提供商均支持)
## 快速开始
```
-- Generate IDs with prefixes
SELECT nanoid('cus_'); -- cus_V1StGXR8_Z5jdHi6B
SELECT nanoid('ord_'); -- ord_K3JwF9HgNxP2mQrTy
SELECT nanoid('user_'); -- user_9LrfQXpAwB3mHkSt
-- Use in tables
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE DEFAULT nanoid('cus_'),
name TEXT NOT NULL
);
```
## 为什么选择 Nanoids
| 特性 | 自增 | UUID | Nanoid |
| ------------------- | ----------------- | ------------- | ------------ |
| **安全性** | 否(暴露数量) | 是 | 是 |
| **长度** | 可变 | 36 个字符 | 21 个字符 |
| **URL 友好** | 是 | 否(包含连字符)| 是 |
| **分布式支持** | 否 | 是 | 是 |
| **性能** | 快速 | 较慢 | 快速 |
## 性能
```
SELECT nanoid('ord_') FROM generate_series(1, 100000);
-- ~0.9s = 110,000 IDs/sec
```
- 生成速度快(100K+ ID/秒)
- 内存效率高
- 分布式系统中无需协调
## 用法
### 基础示例
```
-- Default (21 chars)
SELECT nanoid(); -- V1StGXR8_Z5jdHi6B-myT
-- With prefix
SELECT nanoid('user_'); -- user_V1StGXR8_Z5jdHi6B
SELECT nanoid('ord_'); -- ord_K3JwF9HgNxP2mQrTy
-- Custom size
SELECT nanoid('cus_', 25); -- cus_V1StGXR8_Z5jdHi6B-my
-- Custom alphabet (hex-only)
SELECT nanoid('tx_', 16, '0123456789abcdef'); -- tx_a3f9d2c1b8e4
```
### 生产环境表
```
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE DEFAULT nanoid('cus_'),
name TEXT NOT NULL,
CHECK (public_id ~ '^cus_[0-9a-zA-Z]{17}$')
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE DEFAULT nanoid('ord_'),
customer_id TEXT REFERENCES customers(public_id),
amount DECIMAL(10,2)
);
```
**大小计算:** 默认大小 21,带有前缀 `cus_`(4 个字符)= 17 个随机字符
**提示:**
- 在 `public_id` 上添加 `UNIQUE` 就足够了 - 无需额外索引
- 使用正则表达式的 CHECK 约束速度很快,可用于前缀验证
### 批量生成
```
WITH batch_ids AS (
SELECT nanoid('item_') as id, 'Product ' || generate_series as name
FROM generate_series(1, 100000)
)
INSERT INTO products (public_id, name)
SELECT id, name FROM batch_ids;
-- ~1 second for 100k IDs
```
### 参数
- `prefix` (text, 默认 `''`) - 前置到 ID 的字符串
- `size` (int, 默认 `21`) - 包含前缀的总长度
- `alphabet` (text, 默认 `0-9a-zA-Z`) - 62 个 URL 安全字符,长度必须在 2-255 个字符之间
- `additionalBytesFactor` (float, 默认 `1.02`) - 用于提升效率的缓冲区乘数
### 自定义字母表
```
-- Hex-only IDs
SELECT nanoid('tx_', 16, '0123456789abcdef');
-- tx_a3f9d2c1b8e4
-- Numbers-only (not recommended - less entropy)
SELECT nanoid('ref_', 12, '0123456789');
-- ref_847392
-- URL-safe base64
SELECT nanoid('tok_', 32, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_');
```
## 时间排序 ID(进阶)
如果你需要字典序的时间排序(审计日志、事件流),可以使用 `nanoid_sortable()`。这会在 ID 中嵌入时间戳,这**会暴露创建时间和业务活动模式**。请仅在必要时使用。
**用法:**
```
-- Time-sorted IDs (8 chars timestamp + 9 chars random for size 21 with 4-char prefix)
SELECT nanoid_sortable('log_'); -- log_0uQzNrIEg13LGTj4c
SELECT nanoid_sortable('evt_'); -- evt_0uQzNrIEutvmf1aS
-- Extract timestamp
SELECT nanoid_extract_timestamp('log_0uQzNrIBqK9ayvN1T', 4);
-- 2025-01-15 14:23:10.204
-- Use in tables
CREATE TABLE audit_logs (
id SERIAL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE DEFAULT nanoid_sortable('log_'),
message TEXT
);
```
**权衡:**
- **优点:** 无需单独的时间戳列即可实现字典序的时间排序
- **缺点:** 会暴露创建时间和业务活动模式
- **用例:** 对隐私要求不高的内部审计日志
## 开发
```
# 使用 Docker 克隆和测试
git clone https://github.com/elitan/postgres-nanoid
cd postgres-nanoid
make up && make test-all # Start + run tests
make psql # Connect and try functions
```
点击展开安装 SQL(可直接复制粘贴)
``` CREATE EXTENSION IF NOT EXISTS pgcrypto; DROP FUNCTION IF EXISTS nanoid CASCADE; DROP FUNCTION IF EXISTS nanoid_optimized CASCADE; -- Helper function for random generation CREATE OR REPLACE FUNCTION nanoid_optimized(size int, alphabet text, mask int, step int) RETURNS text LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS $$ DECLARE idBuilder text := ''; counter int := 0; bytes bytea; alphabetIndex int; alphabetArray text[]; alphabetLength int; BEGIN alphabetArray := regexp_split_to_array(alphabet, ''); alphabetLength := array_length(alphabetArray, 1); LOOP bytes := gen_random_bytes(step); FOR counter IN 0..step - 1 LOOP alphabetIndex :=(get_byte(bytes, counter) & mask) + 1; IF alphabetIndex <= alphabetLength THEN idBuilder := idBuilder || alphabetArray[alphabetIndex]; IF length(idBuilder) = size THEN RETURN idBuilder; END IF; END IF; END LOOP; END LOOP; END $$; -- Main nanoid function - secure random IDs CREATE OR REPLACE FUNCTION nanoid( prefix text DEFAULT '', size int DEFAULT 21, alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', additionalBytesFactor float DEFAULT 1.02 ) RETURNS text LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS $$ DECLARE random_size int; random_part text; finalId text; alphabetLength int; mask int; step int; BEGIN IF size IS NULL OR size < 1 THEN RAISE EXCEPTION 'The size must be defined and greater than 0!'; END IF; IF alphabet IS NULL OR length(alphabet) < 2 OR length(alphabet) > 255 THEN RAISE EXCEPTION 'The alphabet must be between 2 and 255 symbols!'; END IF; IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!'; END IF; random_size := size - length(prefix); IF random_size < 1 THEN RAISE EXCEPTION 'The size must be larger than the prefix length! Need at least % characters.', length(prefix) + 1; END IF; alphabetLength := length(alphabet); mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) AS int)) - 1; step := cast(ceil(additionalBytesFactor * mask * random_size / alphabetLength) AS int); IF step > 1024 THEN step := 1024; END IF; random_part := nanoid_optimized(random_size, alphabet, mask, step); finalId := prefix || random_part; RETURN finalId; END $$; ```点击展开可排序 ID 的安装说明
``` -- Add to your existing installation DROP FUNCTION IF EXISTS nanoid_sortable CASCADE; DROP FUNCTION IF EXISTS nanoid_extract_timestamp CASCADE; CREATE OR REPLACE FUNCTION nanoid_sortable( prefix text DEFAULT '', size int DEFAULT 21, alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', additionalBytesFactor float DEFAULT 1.02 ) RETURNS text LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS $$ DECLARE timestamp_ms bigint; timestamp_encoded text := ''; remainder int; temp_ts bigint; random_size int; random_part text; finalId text; alphabetArray text[]; alphabetLength int; mask int; step int; BEGIN IF size IS NULL OR size < 1 THEN RAISE EXCEPTION 'The size must be defined and greater than 0!'; END IF; IF alphabet IS NULL OR length(alphabet) < 2 OR length(alphabet) > 255 THEN RAISE EXCEPTION 'The alphabet must be between 2 and 255 symbols!'; END IF; IF additionalBytesFactor IS NULL OR additionalBytesFactor < 1 THEN RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!'; END IF; timestamp_ms := extract(epoch from clock_timestamp()) * 1000; alphabetArray := regexp_split_to_array(alphabet, ''); alphabetLength := array_length(alphabetArray, 1); temp_ts := timestamp_ms; IF temp_ts = 0 THEN timestamp_encoded := alphabetArray[1]; ELSE WHILE temp_ts > 0 LOOP remainder := temp_ts % alphabetLength; timestamp_encoded := alphabetArray[remainder + 1] || timestamp_encoded; temp_ts := temp_ts / alphabetLength; END LOOP; END IF; WHILE length(timestamp_encoded) < 8 LOOP timestamp_encoded := alphabetArray[1] || timestamp_encoded; END LOOP; random_size := size - length(prefix) - 8; IF random_size < 1 THEN RAISE EXCEPTION 'The size including prefix and timestamp must leave room for random component! Need at least % characters.', length(prefix) + 9; END IF; mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) AS int)) - 1; step := cast(ceil(additionalBytesFactor * mask * random_size / alphabetLength) AS int); IF step > 1024 THEN step := 1024; END IF; random_part := nanoid_optimized(random_size, alphabet, mask, step); finalId := prefix || timestamp_encoded || random_part; RETURN finalId; END $$; CREATE OR REPLACE FUNCTION nanoid_extract_timestamp( nanoid_value text, prefix_length int DEFAULT 0, alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) RETURNS timestamp LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE AS $$ DECLARE timestamp_encoded text; timestamp_ms bigint := 0; alphabetArray text[]; alphabetLength int; char_pos int; i int; BEGIN timestamp_encoded := substring(nanoid_value, prefix_length + 1, 8); alphabetArray := regexp_split_to_array(alphabet, ''); alphabetLength := array_length(alphabetArray, 1); FOR i IN 1..length(timestamp_encoded) LOOP char_pos := array_position(alphabetArray, substring(timestamp_encoded, i, 1)); IF char_pos IS NULL THEN RAISE EXCEPTION 'Invalid character in timestamp: %', substring(timestamp_encoded, i, 1); END IF; timestamp_ms := timestamp_ms * alphabetLength + (char_pos - 1); END LOOP; RETURN to_timestamp(timestamp_ms / 1000.0); EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'Invalid nanoid format or timestamp extraction failed: %', SQLERRM; END $$; ```标签:NanoID, PostgreSQL, 唯一标识符, 数据库扩展, 测试用例, 随机数生成