Skip to content
Search

PostgreSQL:PrimaryKey

lastmod 2026-01-21created 2026-01-213 edits

About the Primary Key (PK) in PostgreSQL.

Using BIGINT as an unpredictable ID

There are a few approaches:

Using a Feistel Cipher

Encrypts a sequential sequence to generate random-looking IDs without collisions.

-- Feistel cipher function for pseudo-random ID generation
-- 페이스텔 암호 기반 의사난수 ID 생성 함수
CREATE OR REPLACE FUNCTION feistel_encrypt(value BIGINT) RETURNS BIGINT AS $$
DECLARE
    l1 BIGINT;
    r1 BIGINT;
    l2 BIGINT;
    r2 BIGINT;
    i  INT;
    key BIGINT := 1234567890;  -- Change this secret key / 비밀키 변경 필요
BEGIN
    l1 := (value >> 32) & 4294967295;
    r1 := value & 4294967295;

    FOR i IN 1..3 LOOP
        l2 := r1;
        r2 := l1 # ((((r1 * key) + i) * 2654435769) & 4294967295);
        l1 := l2;
        r1 := r2;
    END LOOP;

    RETURN (l1 << 32) | r1;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- Create sequence / 시퀀스 생성
CREATE SEQUENCE uid_seq;

-- Usage example / 사용 예시
CREATE TABLE users (
    id BIGINT PRIMARY KEY DEFAULT feistel_encrypt(nextval('uid_seq')),
    name TEXT
);

pgcrypto + collision check

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Random BIGINT with collision check
-- 충돌 체크를 포함한 랜덤 BIGINT 생성
CREATE OR REPLACE FUNCTION generate_random_uid(table_name TEXT, column_name TEXT) 
RETURNS BIGINT AS $$
DECLARE
    new_id BIGINT;
    exists_check BOOLEAN;
BEGIN
    LOOP
        -- Generate random positive BIGINT / 양수 BIGINT 생성
        new_id := abs(('x' || substr(md5(gen_random_bytes(8)::text), 1, 16))::bit(64)::bigint);

        -- Check for collision / 충돌 체크
        EXECUTE format('SELECT EXISTS(SELECT 1 FROM %I WHERE %I = $1)', table_name, column_name)
        INTO exists_check USING new_id;

        IF NOT exists_check THEN
            RETURN new_id;
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

Simple XOR + Shuffle approach

-- Simple XOR-based obfuscation / 간단한 XOR 기반 난독화
CREATE OR REPLACE FUNCTION obfuscate_id(id BIGINT) RETURNS BIGINT AS $$
DECLARE
    secret BIGINT := 6432197583241678905;  -- Secret key / 비밀키
    multiplier BIGINT := 6364136223846793005;
BEGIN
    RETURN ((id * multiplier) # secret) & 9223372036854775807;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

-- Reverse function / 역함수 (필요시)
CREATE OR REPLACE FUNCTION deobfuscate_id(obfuscated BIGINT) RETURNS BIGINT AS $$
DECLARE
    secret BIGINT := 6432197583241678905;
    -- Modular inverse of multiplier / multiplier의 모듈러 역원
    inverse BIGINT := 13877824140714322085;
BEGIN
    RETURN (((obfuscated & 9223372036854775807) # secret) * inverse) & 9223372036854775807;
END;
$$ LANGUAGE plpgsql IMMUTABLE;

Conclusion

Approach

Collision risk

Decryption

Performance

Complexity

Feistel

❌ None

✅ Possible

Fast

Medium

pgcrypto

❌ None (checked)

❌ Not possible

Slow

Low

XOR Shuffle

❌ None

✅ Possible

Very fast

Low

The Feistel approach is the most balanced choice. It never collides, can be decrypted when needed, and looks random enough.

If security is critical, also consider using the Hashids/Sqids libraries at the application level.

See also

author Zer0 sitemap