Skip to content

RANDOM / SETSEED ​

random() 生成 [0, 1) 区间的伪随机浮点数,setseed() 用于设定随机种子以获得可复现结果。看似简单,但它的三个性质决定了正确用法:左闭右开区间、每行重新求值(VOLATILE)、不是密码学安全。最常见的两个灾难是「ORDER BY random() LIMIT 10 打爆全表排序」和「用 round(random()) 得到分布不均的 0/1」。本节把区间换算、可复现性、大表抽样、测试数据生成四类问题讲透。

前置知识

阅读本节前,建议先了解:SQRT / CBRT / EXP / LN / LOG

基础概念 ​

概念术语说明
伪随机数Pseudo-Random Number由确定性算法生成,非真随机
随机种子Seed决定随机序列起点的初始值
可复现性Reproducibility同一种子产生同一序列
左闭右开Half-Open Interval[0, 1):含 0、不含 1
会话Session一个客户端连接,种子仅在会话内生效
函数易变性VolatilityVOLATILE 函数每行重新求值
均匀分布Uniform Distribution区间内每个值等概率
正态分布Normal Distribution钟形分布,由均值与标准差决定,random_normal() 生成
均值Mean正态分布的中心位置参数
标准差Standard Deviation正态分布的离散程度参数
抽样Sampling从总体中取出子集
表采样TABLESAMPLEPostgreSQL 内建的高速抽样语法
密码学安全Cryptographically Secure结果不可从历史推测,random() 不满足
测试数据Test Fixture用于压测与验证的合成数据

random() 的四个关键性质:

性质表现后果
区间返回 [0, 1),可能为 0,不可能为 1乘 N 后最大值为 N-ε
易变性标记为 VOLATILE每行/每次调用都重新求值
精度返回 double precision(53 位有效尾数)可区分约 2⁵³ 个不同值
可播种setseed() 影响当前会话种子只对本连接有效,连接池下不可靠

random() 不可能返回 1

这看起来是细节,但直接影响区间换算。random() * 10 的结果范围是 [0, 10),永远不会是 10。若你想取 1..10 的整数,写 floor(random() * 10) + 1,不要写 ceil(random() * 10) —— 后者在 random() 返回 0 时得到 0,超出预期区间。

区间换算对照表(这是本节最该记住的一张表):

目标区间正确写法常见错误写法错误原因
[0, 1)random()—原生行为
[0, 10)random() * 10—原生行为
1..10(整数)floor(random() * 10) + 1ceil(random() * 10)random()=0 时得到 0
0..9(整数)floor(random() * 10)round(random() * 10)round 使两端概率减半
5..15(整数)floor(random() * 11) + 5round(random() * 10) + 5分布不均
[a, b)random() * (b - a) + a——
-1..1(整数)floor(random() * 3) - 1sign(random() - 0.5)0 的概率为 0

一句话记忆:floor(random() * 区间长度) + 左端点,这是整数随机数的唯一正确公式。

语法与用法 ​

sql
-- 生成 [0, 1) 的伪随机数
random()

-- 带范围的随机数(PostgreSQL 17+,闭区间 [min, max])
random(min, max)                          -- integer / bigint / numeric 三种重载

-- 正态分布随机数(PostgreSQL 16+)
random_normal()                           -- 标准正态 N(0, 1)
random_normal(mean)                       -- 指定均值
random_normal(mean, stddev)               -- 指定均值与标准差

-- 设定随机种子(返回 void)
setseed(seed)          -- seed 为 double precision,范围 [-1, 1]

-- 内建表采样(大表抽样的正确姿势)
SELECT * FROM table_name TABLESAMPLE SYSTEM (percent);
SELECT * FROM table_name TABLESAMPLE BERNOULLI (percent);

-- 相关配套
generate_series(start, stop [, step])    -- 生成序列,用于造数
-- pgcrypto 扩展(密码学安全,需 CREATE EXTENSION pgcrypto)
gen_random_bytes(n)
gen_random_uuid()

最小可运行示例:

sql
-- 生成几个随机数
SELECT
  random()                     AS r1,
  random()                     AS r2,
  floor(random() * 10)::int    AS int_0_9,
  floor(random() * 10 + 1)::int AS int_1_10,
  floor(random() * 11 + 5)::int AS int_5_15;
text
         r1          |         r2          | int_0_9 | int_1_10 | int_5_15
---------------------+---------------------+---------+----------+----------
 0.35678901234567890 | 0.91234567890123456 |       3 |        9 |       12

准备本节贯穿使用的示例表:

sql
-- 用户表:用于抽样、随机排序、造数演示
CREATE TABLE users (
  id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  username    TEXT NOT NULL,
  email       TEXT NOT NULL,
  city        TEXT NOT NULL,
  score       INTEGER NOT NULL DEFAULT 0,
  is_active   BOOLEAN NOT NULL DEFAULT true,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 用 generate_series 造 10000 条测试数据
INSERT INTO users (username, email, city, score, is_active)
SELECT
  'user_' || lpad(i::text, 6, '0'),
  'user_' || lpad(i::text, 6, '0') || '@example.com',
  (ARRAY['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市'])[floor(random() * 6 + 1)],
  floor(random() * 1000)::int,
  random() < 0.8                                   -- 80% 为活跃用户
FROM generate_series(1, 10000) AS i;

-- 抽奖活动表
CREATE TABLE lottery_prizes (
  id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  prize_name  TEXT NOT NULL,
  stock       INTEGER NOT NULL,
  weight      INTEGER NOT NULL      -- 权重,越大中奖概率越高
);

INSERT INTO lottery_prizes (prize_name, stock, weight) VALUES
  ('一等奖 · 机械键盘',  1,   1),
  ('二等奖 · 无线鼠标',  5,   5),
  ('三等奖 · 鼠标垫',   20,  20),
  ('参与奖 · 5 元券',  500, 200);
sql
-- 基础演示:随机排序与随机抽样
SELECT
  count(*)                                    AS total_users,
  count(*) FILTER (WHERE is_active)           AS active_users
FROM users;
text
 total_users | active_users
-------------+--------------
       10000 |         8012
sql
-- 简单随机抽样:随机取 5 个用户(小表可用,大表慎用)
SELECT id, username, city, score
FROM users
ORDER BY random()
LIMIT 5;
text
  id   |   username    |  city  | score
-------+---------------+--------+-------
  1842 | user_001842   | 杭州市 |   417
  6503 | user_006503   | 深圳市 |   836
   927 | user_000927   | 成都市 |    92
  7710 | user_007710   | 上海市 |   623
  3388 | user_003388   | 北京市 |   255

详细说明 ​

区间换算:正确公式与常见错误 ​

正确公式:要生成 [lo, hi] 闭区间的整数,用

sql
floor(random() * (hi - lo + 1)) + lo
sql
-- 区间换算实测:生成 1..6 的骰子点数
SELECT
  floor(random() * 6)::int + 1       AS dice_correct,
  ceil(random() * 6)::int            AS dice_wrong_ceil,
  round(random() * 6)::int           AS dice_wrong_round
FROM generate_series(1, 1);
text
 dice_correct | dice_wrong_ceil | dice_wrong_round
--------------+-----------------+------------------
            4 |               5 |                6

为什么 ceil 是错的:当 random() 返回 0 时,ceil(0 * 6) = 0,得到 0 点。虽然概率极低(约 2⁻⁵³),但在海量数据生成中必然出现。

sql
-- 验证 ceil 的错误:强制 random 为 0 的效果
SELECT
  ceil(0 * 6)::int          AS ceil_at_zero,     -- 0(越界!)
  floor(0 * 6)::int + 1     AS floor_at_zero;    -- 1(正确)
text
 ceil_at_zero | floor_at_zero
--------------+---------------
            0 |             1

为什么 round 是错的:round(random() * 6) 中,只有当 random() * 6 ∈ [5.5, 6) 时结果才为 6,区间长度 0.5;而结果 1 需要 [0.5, 1.5),长度 1.0。两端概率只有中间的一半,这就是分布不均:

sql
-- 用大量样本验证 round 的分布偏差(理论:1 和 6 各占 1/12,2~5 各占 1/6)
WITH samples AS (
  SELECT
    floor(random() * 6)::int + 1 AS correct_val,
    round(random() * 6)::int     AS round_val
  FROM generate_series(1, 60000)
)
SELECT
  correct_val,
  count(*) FILTER (WHERE correct_val = correct_val) AS correct_cnt,
  count(*) FILTER (WHERE round_val = correct_val)   AS round_cnt,
  round(100.0 * count(*) FILTER (WHERE round_val = correct_val) / 60000, 2) AS round_pct
FROM samples
GROUP BY correct_val
ORDER BY correct_val;
text
 correct_val | correct_cnt | round_cnt | round_pct
-------------+-------------+-----------+-----------
           1 |       10012 |      5003 |      8.34
           2 |        9987 |     10021 |     16.70
           3 |       10034 |      9978 |     16.63
           4 |        9961 |     10045 |     16.74
           5 |        9990 |      9942 |     16.57
           6 |       10016 |      5011 |      8.35

上表清楚显示:round 让两端(1 和 6)的概率只有中间值的一半。这类偏差在抽奖、分流、A/B 测试中会直接造成业务损失。

不要用 round(random()) 做布尔分流

round(random()) 的分布是 0 占 50%([0, 0.5))、1 占 50%([0.5, 1))—— 这个特例恰好均匀,但极其容易误推广。分流应显式用比较:

sql
-- 正确:按 30% / 70% 分流
SELECT
  CASE WHEN random() < 0.3 THEN 'group_a' ELSE 'group_b' END AS ab_group_text,
  (random() < 0.3)::int                                      AS ab_group_flag
FROM users
LIMIT 5;

random(min, max):带范围的重载(PostgreSQL 17+) ​

PostgreSQL 17 起,random() 不再只有无参形式,而是新增了三个带范围的重载,直接省掉了手写区间换算:

签名返回类型说明
random(min integer, max integer)integer返回 min <= x <= max 的整数
random(min bigint, max bigint)bigint返回 min <= x <= max 的整数
random(min numeric, max numeric)numeric返回 min <= x <= max 的数值
sql
-- PostgreSQL 17+:带范围的重载,区间为闭区间 [min, max]
SELECT
  random(1, 10)          AS dice,          -- 1..10 的整数
  random(-0.499, 0.499)  AS centered,      -- 小数区间
  random(100::bigint, 200::bigint) AS big;
text
 dice | centered | big
------+----------+-----
    7 |    0.347 | 147

注意区间语义与无参版不同:无参 random() 是左闭右开 [0.0, 1.0),而 random(min, max) 是闭区间 min <= x <= max,两端都可能取到。

numeric 重载的小数位规则

传入 numeric 时,结果的小数位数等于 min 与 max 中较多的一方。例如 random(0.0, 1.0) 会返回一位小数的 numeric(如 0.3),而不是 double precision。这既避免了浮点尾数过长,也让结果与业务字段的精度保持一致。

版本提示:如果你的环境低于 PostgreSQL 17,random(1, 10) 会报「函数不存在」,必须退回手写公式 floor(random() * 10) + 1。这一点在跨环境部署时需特别注意——同样的 SQL 在 16 与 17 上行为不一致。

sql
-- 兼容旧版本的可移植写法(无版本依赖)
SELECT floor(random() * (10 - 1 + 1))::int + 1 AS dice_portable;

random_normal:正态分布随机数(PostgreSQL 16+) ​

真实业务数据大多不是均匀分布:用户身高、订单金额、页面停留时长通常近似正态分布。random_normal() 直接生成符合正态分布的随机值,避免用「多个 random() 求和」这类粗糙近似。

签名返回类型说明
random_normal()double precision标准正态分布 N(0, 1)
random_normal(mean double precision)double precision指定均值,标准差默认为 1.0
random_normal(mean double precision, stddev double precision)double precision指定均值与标准差
sql
-- PostgreSQL 16+:生成正态分布随机数
SELECT
  random_normal()                     AS standard,    -- N(0, 1)
  random_normal(170.0, 5.0)           AS height_cm,   -- 均值 170、标准差 5
  random_normal(100.0, 20.0)          AS amount;
text
      standard      |     height_cm     |      amount
--------------------+-------------------+------------------
 0.0512854191273645 | 172.4839201746213 | 118.299471002318
sql
-- 用正态分布生成更真实的用户订单金额(大多数居中,少数极端)
CREATE TABLE orders (
  id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    TEXT NOT NULL,
  amount      NUMERIC(10,2) NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO orders (order_no, amount)
SELECT
  'SO' || lpad(i::text, 8, '0'),
  round(greatest(random_normal(200, 60), 1)::numeric, 2)  -- 夹取正数,避免负金额
FROM generate_series(1, 5000) AS i;
text
  min_amount |  max_amount | avg_amount
-------------+-------------+------------
        1.00 |      612.35 |     199.87

注意上面用 greatest(..., 1) 夹取下界:正态分布理论上是对称无界的,会生成负值,而订单金额不能为负。

random_normal 与 random 共享同一个随机源

官方明确指出:random() 与 random_normal() 使用同一个确定性伪随机数生成器。因此 setseed() 会同时影响这两个函数,两次调用同一序列的结果也可通过重新 setseed() 复现。这意味着在同一个会话中交错调用 random() 与 random_normal() 会互相消耗随机序列,导致「只对 random() 设定种子」的复现实验失效。若需要严格的独立复现,请在每次实验前重新 setseed()。

setseed:可复现性(仅限当前会话) ​

setseed(seed) 设定随机种子,参数是 double precision,取值范围 [-1, 1]。

sql
-- 同一种子产生同一序列(在同一个会话中连续执行)
SELECT setseed(0.42);
SELECT random() AS first_run_1, random() AS first_run_2;

SELECT setseed(0.42);
SELECT random() AS second_run_1, random() AS second_run_2;
text
 first_run_1         | first_run_2
---------------------+---------------------
 0.01135244979245708 | 0.13111902428552886

 second_run_1        | second_run_2
---------------------+---------------------
 0.01135244979245708 | 0.13111902428552886

两次结果完全相同。这就是可复现性:测试用例、数据生成、A/B 分流的固定基线都依赖它。

种子只在当前会话生效

setseed 影响的是当前数据库会话的随机数生成器状态,不会写回数据库,也不影响其他连接。

这意味着:

  1. 连接池下不可靠。应用通过连接池取到哪个连接是不确定的,setseed 可能作用在错误的连接上。
  2. 多线程/多连接无法共享种子。
  3. ORDER BY random() 的执行顺序不确定,即使设了种子,如果查询计划变化(如并行扫描),结果也可能不同。

因此 setseed 主要用于测试脚本、数据初始化、本地复现问题,不要用在生产代码的关键路径。

sql
-- 用 setseed 生成可复现的测试数据
SELECT setseed(0.2026);

INSERT INTO users (username, email, city, score)
SELECT
  'seed_user_' || lpad(i::text, 4, '0'),
  'seed_user_' || lpad(i::text, 4, '0') || '@test.local',
  (ARRAY['北京市', '上海市', '广州市'])[floor(random() * 3 + 1)],
  floor(random() * 100)::int
FROM generate_series(1, 100) AS i;
text
INSERT 0 100
sql
-- 验证可复现:同一查询在重设种子后结果一致
SELECT setseed(0.9);
SELECT id, username, city FROM users WHERE username LIKE 'seed_user_%' ORDER BY random() LIMIT 3;
text
  id   |   username    |  city
-------+---------------+--------
 10042 | seed_user_0042 | 上海市
 10019 | seed_user_0019 | 北京市
 10087 | seed_user_0087 | 广州市

ORDER BY random() LIMIT n 的性能灾难 ​

这是 random() 最著名的问题。ORDER BY random() 会为每一行生成一个随机数、构建排序集、全表排序,然后只取前 N 行。复杂度是 O(n log n),且必须扫描全部行。

sql
-- 不推荐!大表上会扫描全部行并排序
SELECT * FROM users ORDER BY random() LIMIT 10;
text
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Limit  (cost=1234.56..1234.59 rows=10 width=68) (actual time=45.123..45.125 rows=10)
   ->  Sort  (cost=1234.56..1259.56 rows=10000 width=68) (actual time=45.120..45.122 rows=10)
         Sort Key: (random())
         Sort Method: top-N heapsort  Memory: 27kB
         ->  Seq Scan on users  (cost=0.00..183.00 rows=10000 width=68) (actual time=0.008..20.345 rows=10000)
 Planning Time: 0.123 ms
 Execution Time: 45.234 ms

注意 Seq Scan 读出全部 10000 行,加上 Sort。数据量到千万级时,这条语句会跑几十秒并占满内存。

正确做法一:TABLESAMPLE 语法(PostgreSQL 内建,直接块级或行级采样):

sql
-- SYSTEM:按物理块采样,速度快,但数据分布可能不均
SELECT count(*) FROM users TABLESAMPLE SYSTEM (1);
text
 count
-------
    96
sql
-- BERNOULLI:按行采样,分布更均匀,但需要扫全表
SELECT count(*) FROM users TABLESAMPLE BERNOULLI (1);
text
 count
-------
    103
采样方式采样单位速度准确性可否配合 WHERE
SYSTEM (p)物理数据块极快块内相关性高时偏差大可,但先采样后过滤
BERNOULLI (p)行慢(全表扫描)均匀可,但先采样后过滤
ORDER BY random()行最慢精确全表参与排序
sql
-- TABLESAMPLE 的百分比可用变量(PostgreSQL 支持常量与部分表达式)
SELECT * FROM users TABLESAMPLE SYSTEM (0.5) REPEATABLE (42) LIMIT 5;
text
  id   |   username    |  city  | score | is_active |     created_at
-------+---------------+--------+-------+-----------+---------------------
   118 | user_000118   | 上海市 |   523 | t         | 2026-09-24 10:00:00+08
   391 | user_000391   | 北京市 |   871 | t         | 2026-09-24 10:00:00+08
  1204 | user_001204   | 深圳市 |    66 | f         | 2026-09-24 10:00:00+08
  1522 | user_001522   | 杭州市 |   308 | t         | 2026-09-24 10:00:00+08
  1899 | user_001899   | 成都市 |   944 | t         | 2026-09-24 10:00:00+08

REPEATABLE (seed) 让采样可复现

TABLESAMPLE SYSTEM (1) REPEATABLE (42) 中的 REPEATABLE 指定采样种子,同一张表 + 同一种子会得到相同的样本集合。这是 TABLESAMPLE 优于 ORDER BY random() 的另一个理由。

正确做法二:基于主键的随机起点 + 有序扫描(适合需要精确 N 行的场景):

sql
-- 随机起点 + 顺序扫描:只需索引扫描,代价 O(N)
WITH bounds AS (
  SELECT
    min(id)                                                    AS min_id,
    max(id)                                                    AS max_id
  FROM users
),
start_point AS (
  SELECT floor(random() * (max_id - min_id + 1) + min_id)::int AS start_id
  FROM bounds
)
SELECT u.id, u.username, u.city
FROM users u
CROSS JOIN start_point s
WHERE u.id >= s.start_id
ORDER BY u.id
LIMIT 5;
text
  id   |   username    |  city
-------+---------------+--------
  6521 | user_006521   | 杭州市
  6522 | user_006522   | 成都市
  6523 | user_006523   | 北京市
  6524 | user_006524   | 上海市
  6525 | user_006525   | 深圳市

这个方案的代价是「随机性不够随机」(只随机起点,取连续 N 行),但性能是 ORDER BY random() 的数百倍。若主键有空洞,取到的行数可能不足 N 行,可循环重试。

VOLATILE:每行重新求值 ​

random() 被标记为 VOLATILE(易变),意味着优化器不会缓存它的结果,每次出现都会重新求值一次。

sql
-- 每次调用都返回不同值
SELECT
  random() AS a,
  random() AS b,
  random() = random() AS same;      -- f:三次调用三个不同值
text
         a           |         b           | same
---------------------+---------------------+------
 0.12345678901234567 | 0.98765432109876543 | f
sql
-- INSERT ... SELECT 中每行都会重新求值
INSERT INTO test_random (val)
SELECT random() FROM generate_series(1, 3);
-- 三行得到三个不同的随机值

这条性质的一个经典陷阱:想在 UPDATE 中用同一个随机值更新一批行,直接写 UPDATE ... SET col = random() 会得到每行都不同的值:

sql
-- 不推荐!每行都会得到不同的随机值
UPDATE users SET score = floor(random() * 100);

-- 正确:一次算好,作为常量使用
WITH r AS (SELECT floor(random() * 100)::int AS v)
UPDATE users SET score = r.v FROM r;

另一个经典陷阱:random() 与窗口函数/DISTINCT 组合时的意外行为:

sql
-- random() 让 DISTINCT 失去意义(每行都不同)
SELECT DISTINCT random() FROM generate_series(1, 5);
text
       random
---------------------
 0.12345678901234567
 0.87654321098765432
 0.45678901234567890
 0.65432109876543210
 0.23456789012345678
sql
-- 正确的随机排序:先生成随机值,再按它排序(PostgreSQL 的 ORDER BY random() 已隐含此逻辑)
SELECT username
FROM users
ORDER BY random()
LIMIT 3;

为什么 random() 一定是 VOLATILE

如果 random() 是 STABLE 或 IMMUTABLE,优化器可能把它折叠成常量(例如在同一次查询中复用同一个随机数),那 ORDER BY random() 会让所有行的排序键相同,随机排序完全失效。标记为 VOLATILE 是功能正确性的要求,不是实现细节。

非密码学安全 ​

random() 使用的是 xorshift 系列伪随机算法(PostgreSQL 内部为 pg_prng),速度快但可预测。不适合:

  • 生成会话令牌(Session Token)
  • 生成密码重置链接
  • 生成 API Key
  • 任何攻击者可能推理出下一个值的场景
sql
-- 不推荐!用于安全场景
SELECT md5(random()::text) AS bad_token;
sql
-- 正确:用 pgcrypto 扩展(需先安装)
CREATE EXTENSION IF NOT EXISTS pgcrypto;

SELECT
  encode(gen_random_bytes(32), 'hex')   AS secure_token,      -- 256 位随机
  gen_random_uuid()                     AS uuid_val;          -- UUID v4
text
                              secure_token                               |              uuid_val
-------------------------------------------------------------------------+--------------------------------------
 3f8a2b1c9d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8 | 550e8400-e29b-41d4-a716-446655440000

random() 绝不能用于安全场景

PostgreSQL 官方文档明确指出 random() 不适合密码学用途。令牌、密码、加密密钥、会话 ID 一律使用 pgcrypto 的 gen_random_bytes() 或 gen_random_uuid(),或更推荐由应用层的 crypto.randomBytes() 生成。

另外注意:gen_random_uuid() 从 PostgreSQL 13 起已内置(无需 pgcrypto),而 gen_random_bytes() 仍需 pgcrypto 扩展。

用 generate_series 批量造测试数据 ​

generate_series 与 random() 组合是造测试数据的主力:

sql
-- 造订单测试数据:时间随机、金额随机、状态按概率分布
CREATE TABLE orders (
  id          INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    TEXT NOT NULL,
  user_id     INTEGER NOT NULL,
  total_fen   BIGINT NOT NULL,
  status      TEXT NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL
);

INSERT INTO orders (order_no, user_id, total_fen, status, created_at)
SELECT
  'SO' || lpad(i::text, 11, '0'),
  floor(random() * 10000 + 1)::int,                                  -- 随机用户
  floor(random() * 500000 + 1000)::bigint,                           -- 10.00 ~ 5000.99 元
  CASE
    WHEN random() < 0.70 THEN 'paid'
    WHEN random() < 0.85 THEN 'shipped'
    WHEN random() < 0.95 THEN 'done'
    ELSE 'refund'
  END,                                                               -- 加权状态分布
  now() - (random() * 90 || ' days')::interval                       -- 近 90 天随机时间
FROM generate_series(1, 20000) AS i;
text
INSERT 0 20000
sql
-- 校验生成的数据分布
SELECT
  status,
  count(*)                                          AS cnt,
  round(100.0 * count(*) / sum(count(*)) OVER (), 2) AS pct,
  round(avg(total_fen) / 100.0, 2)                   AS avg_yuan
FROM orders
GROUP BY status
ORDER BY cnt DESC;
text
 status  |  cnt  |  pct  | avg_yuan
---------+-------+-------+----------
 paid    | 13982 | 69.91 |  2504.13
 shipped |  3021 | 15.11 |  2498.76
 done    |  2003 | 10.02 |  2507.44
 refund  |   994 |  4.97 |  2496.89

加权随机:抽奖与流量分配 ​

纯均匀随机无法满足「一等奖概率 1%、二等奖 5%」这类需求,需要加权随机。实现方式是把权重累加,再与随机数比较:

sql
-- 加权抽奖:用累计权重 + random() 选出一个奖项
WITH weights AS (
  SELECT
    prize_name,
    weight,
    sum(weight) OVER (ORDER BY id)                        AS cum_weight,   -- 累计权重
    sum(weight) OVER ()                                   AS total_weight
  FROM lottery_prizes
)
SELECT prize_name, weight, cum_weight, total_weight
FROM weights
ORDER BY cum_weight;
text
     prize_name      | weight | cum_weight | total_weight
---------------------+--------+------------+--------------
 一等奖 · 机械键盘   |      1 |          1 |          226
 二等奖 · 无线鼠标   |      5 |          6 |          226
 三等奖 · 鼠标垫     |     20 |         26 |          226
 参与奖 · 5 元券     |    200 |        226 |          226
sql
-- 单次抽奖:随机数落在哪个累计区间就中哪个奖
WITH weights AS (
  SELECT
    prize_name,
    sum(weight) OVER (ORDER BY id)   AS cum_weight,
    sum(weight) OVER ()              AS total_weight
  FROM lottery_prizes
),
draw AS (
  SELECT random() * (SELECT total_weight FROM weights LIMIT 1) AS r
)
SELECT
  w.prize_name,
  round(d.r, 4) AS random_point
FROM weights w
CROSS JOIN draw d
WHERE d.r <= w.cum_weight
ORDER BY w.cum_weight
LIMIT 1;
text
     prize_name      | random_point
---------------------+--------------
 参与奖 · 5 元券     |      187.4321
sql
-- 批量抽奖 10000 次,验证概率是否符合权重
WITH weights AS (
  SELECT
    prize_name,
    sum(weight) OVER (ORDER BY id)   AS cum_weight,
    sum(weight) OVER ()              AS total_weight
  FROM lottery_prizes
),
total AS (
  SELECT total_weight FROM weights LIMIT 1
),
draws AS (
  SELECT random() * (SELECT total_weight FROM total) AS r
  FROM generate_series(1, 10000)
)
SELECT
  w.prize_name,
  count(d.r)                                              AS hit_cnt,
  w.weight,
  round(100.0 * count(d.r) / 10000, 3)                    AS actual_pct,
  round(100.0 * w.weight / (SELECT total_weight FROM total), 3) AS expected_pct
FROM weights w
LEFT JOIN draws d ON d.r <= w.cum_weight
  AND d.r > coalesce(
    (SELECT max(cum_weight) FROM weights w2 WHERE w2.cum_weight < w.cum_weight), 0)
GROUP BY w.prize_name, w.weight, w.cum_weight
ORDER BY w.cum_weight;
text
     prize_name      | hit_cnt | weight | actual_pct | expected_pct
---------------------+---------+--------+------------+--------------
 一等奖 · 机械键盘   |      46 |      1 |      0.460 |        0.442
 二等奖 · 无线鼠标   |     224 |      5 |      2.240 |        2.212
 三等奖 · 鼠标垫     |     857 |     20 |      8.570 |        8.850
 参与奖 · 5 元券     |    8873 |    200 |     88.730 |       88.496

实战示例 ​

场景一:大数据表的高效抽样 ​

千万级数据表上做抽样分析,ORDER BY random() 完全不可用。用 TABLESAMPLE 与「分桶抽样」替代:

sql
-- 方案 A:TABLESAMPLE 块级抽样,速度最快
SELECT
  status,
  count(*)          AS sample_cnt,
  round(avg(total_fen) / 100.0, 2) AS avg_yuan
FROM orders TABLESAMPLE SYSTEM (2)
GROUP BY status
ORDER BY sample_cnt DESC;
text
 status  | sample_cnt | avg_yuan
---------+------------+----------
 paid    |        281  |  2501.33
 shipped |         59  |  2487.21
 done    |         41  |  2512.88
 refund  |         19  |  2503.62
sql
-- 方案 B:按主键取模分桶,抽取 1/50 的数据(可走索引,稳定可复现)
SELECT
  status,
  count(*)                         AS sample_cnt,
  round(avg(total_fen) / 100.0, 2) AS avg_yuan
FROM orders
WHERE id % 50 = 7
GROUP BY status
ORDER BY sample_cnt DESC;
text
 status  | sample_cnt | avg_yuan
---------+------------+----------
 paid    |        278  |  2508.44
 shipped |         61  |  2496.72
 done    |         41  |  2501.09
 refund  |         20  |  2493.15

采样方案选择

场景推荐方案
快速估算总量、平均值TABLESAMPLE SYSTEM (p)
需要精确行比例、数据分布均匀TABLESAMPLE BERNOULLI (p)
需要可复现的固定样本TABLESAMPLE ... REPEATABLE (seed)
小表取随机 N 行ORDER BY random() LIMIT n
大表取随机 N 行主键随机起点 + 顺序扫描

场景二:A/B 测试用户分桶 ​

A/B 测试要求「同一用户始终落在同一组」(稳定性)与「组间人数均衡」(均匀性)。用用户 ID 的哈希而非 random(),因为 random() 每次调用结果不同:

sql
-- 正确:用 hashtext 或 md5 做稳定分桶
SELECT
  id,
  username,
  (hashtext(username)::bigint & 9223372036854775807) % 100      AS bucket_0_99,
  CASE
    WHEN (hashtext(username)::bigint & 9223372036854775807) % 100 < 50 THEN 'control'
    WHEN (hashtext(username)::bigint & 9223372036854775807) % 100 < 80 THEN 'variant_a'
    ELSE 'variant_b'
  END                                                           AS ab_group
FROM users
ORDER BY id
LIMIT 8;
text
  id  |   username    | bucket_0_99 | ab_group
------+---------------+-------------+----------
    1 | user_000001   |          42 | control
    2 | user_000002   |          87 | variant_b
    3 | user_000003   |          13 | control
    4 | user_000004   |          66 | variant_a
    5 | user_000005   |          91 | variant_b
    6 | user_000006   |          28 | control
    7 | user_000007   |          74 | variant_a
    8 | user_000008   |          55 | variant_a
sql
-- 验证三组人数是否均衡
SELECT
  CASE
    WHEN bucket < 50 THEN 'control'
    WHEN bucket < 80 THEN 'variant_a'
    ELSE 'variant_b'
  END                     AS ab_group,
  count(*)                AS cnt,
  round(100.0 * count(*) / sum(count(*)) OVER (), 2) AS pct
FROM (
  SELECT (hashtext(username)::bigint & 9223372036854775807) % 100 AS bucket
  FROM users
) AS b
GROUP BY 1
ORDER BY 1;
text
 ab_group  |  cnt  |  pct
-----------+-------+-------
 control   |  5013 | 50.13
 variant_a |  3011 | 30.11
 variant_b |  1976 | 19.76

A/B 分流绝不能用 random()

random() 是 VOLATILE 的,同一个用户在不同请求中会被分到不同组,实验结果完全失效。分流必须用确定性哈希(hashtext / md5 / crc32),保证「同一输入 → 同一分组」。

场景三:可复现的压测数据生成 ​

压测需要「数据随机但可复现」,避免每次生成的基线不同导致结果无法对比:

sql
-- 用固定种子生成可复现的压测数据
SELECT setseed(0.2026);   -- 固定基线

CREATE TABLE bench_events (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  event_type  TEXT NOT NULL,
  user_id     INTEGER NOT NULL,
  amount_fen  BIGINT NOT NULL,
  payload     TEXT NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL
);

INSERT INTO bench_events (event_type, user_id, amount_fen, payload, created_at)
SELECT
  (ARRAY['login', 'view', 'cart', 'order', 'pay'])[floor(random() * 5 + 1)],
  floor(random() * 100000 + 1)::int,
  floor(random() * 1000000)::bigint,
  repeat('x', floor(random() * 200 + 20)::int),
  now() - (random() * 30 || ' days')::interval
FROM generate_series(1, 50000) AS i;
text
INSERT 0 50000
sql
-- 分布校验
SELECT
  event_type,
  count(*)                                              AS cnt,
  round(100.0 * count(*) / sum(count(*)) OVER (), 2)     AS pct,
  round(avg(amount_fen) / 100.0, 2)                     AS avg_yuan,
  round(avg(length(payload)), 1)                         AS avg_payload_len
FROM bench_events
GROUP BY event_type
ORDER BY cnt DESC;
text
 event_type |  cnt  |  pct  | avg_yuan | avg_payload_len
------------+-------+-------+----------+-----------------
 view       | 10031 | 20.06 |  4990.12 |           119.4
 cart       | 10024 | 20.05 |  5003.78 |           120.1
 order      |  9987 | 19.97 |  4996.44 |           119.8
 pay        |  9978 | 19.96 |  5001.09 |           120.3
 login      |  9980 | 19.96 |  5002.55 |           119.6

场景四:用正态分布生成贴近真实的数据 ​

压测数据若全部服从均匀分布,会掩盖真实场景中的长尾问题。用 random_normal() 生成「多数居中、少数极端」的数据更接近生产:

sql
-- PostgreSQL 16+:生成近似真实的订单金额与用户活跃度
CREATE TABLE user_metrics (
  id            INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id       INTEGER NOT NULL,
  order_amount  NUMERIC(10, 2) NOT NULL,
  session_sec   INTEGER NOT NULL,
  login_count   INTEGER NOT NULL
);

INSERT INTO user_metrics (user_id, order_amount, session_sec, login_count)
SELECT
  i,
  -- 金额:均值 200 元、标准差 60 元,夹取到 1 元以上
  round(greatest(random_normal(200, 60), 1)::numeric, 2),
  -- 停留时长:均值 300 秒、标准差 80 秒,夹取到 5 秒以上
  greatest(round(random_normal(300, 80))::int, 5),
  -- 登录次数:均值 8 次、标准差 3 次,夹取到 0 以上
  greatest(round(random_normal(8, 3))::int, 0)
FROM generate_series(1, 20000) AS i;
sql
-- 验证生成数据的分布形态(正态分布应呈现中间多、两端少)
SELECT
  width_bucket(order_amount, 0, 400, 8) AS bucket,
  count(*)                              AS cnt,
  min(order_amount)                     AS bucket_min,
  max(order_amount)                     AS bucket_max
FROM user_metrics
GROUP BY bucket
ORDER BY bucket;
text
 bucket | cnt  | bucket_min | bucket_max
--------+------+------------+------------
      1 |  236 |       1.00 |      49.99
      2 | 1204 |      50.00 |      99.99
      3 | 3808 |     100.00 |     149.99
      4 | 6741 |     150.00 |     199.99
      5 | 5467 |     200.00 |     249.99
      6 | 2038 |     250.00 |     299.99
      7 |  428 |     300.00 |     349.99
      8 |   78 |     350.00 |     399.99

可以看到中间两个桶(4、5)数据最多、两端最少——这正是正态分布的钟形特征,而均匀分布会得到每桶约 2500 条的平坦结果。

需要更高版本能力时怎么做

如果你的环境是 PostgreSQL 15 或更低,random_normal() 不可用。此时可用中心极限定理近似:把 12 个 random() 相加再减去 6,得到一个近似 N(0, 1) 的值:

sql
-- 旧版本近似正态分布(PG 15 及以下)
SELECT
  (SELECT sum(random()) FROM generate_series(1, 12)) - 6 AS approx_normal;

注意事项 ​

  1. random() 返回 [0, 1),不含 1。所以 ceil(random() * 10) 可能返回 0,必须用 floor(random() * n) + 1。

  2. 整数随机数的唯一正确公式是 floor(random() * (hi - lo + 1)) + lo。round 会让两端概率减半。

  3. setseed 只影响当前会话,连接池下不可靠,不要用于生产关键路径。

  4. random() 是 VOLATILE,同一 SELECT 中的多次调用互不相同,UPDATE ... SET c = random() 会让每行得到不同值。

  5. ORDER BY random() 会全表扫描 + 全表排序。大表必须改用 TABLESAMPLE 或主键随机起点。

  6. TABLESAMPLE SYSTEM 按块采样,块内数据相关性高时结果偏差大;需要均匀性时用 BERNOULLI。

  7. TABLESAMPLE 在 WHERE 之前生效,所以 TABLESAMPLE 与 WHERE 一起用时,实际样本量可能远小于预期。

  8. random() 不是密码学安全。令牌、密钥、会话 ID 必须用 gen_random_bytes() / gen_random_uuid()。

  9. gen_random_uuid() 从 PostgreSQL 13 起内置,gen_random_bytes() 仍需 pgcrypto 扩展。

  10. random() 与 DISTINCT 一起用会让 DISTINCT 失效,因为每行都是不同的随机值。

  11. 不要在 WHERE 条件中直接用 random() 过滤,语义容易误解,且无法利用统计信息:

    sql
    -- 不推荐!语义模糊且执行计划不可预测
    SELECT * FROM users WHERE random() < 0.1;
    -- 推荐:明确用抽样语法
    SELECT * FROM users TABLESAMPLE BERNOULLI (10);
  12. random() 在并行查询中每个 worker 独立,ORDER BY random() 的结果不可复现(即使设了种子)。

  13. setseed 的参数必须在 [-1, 1] 内,超出范围会报错:

    sql
    SELECT setseed(2);    -- 错误!ERROR: setseed parameter 2 is out of allowed range [-1,1]
  14. setseed 返回 void,不能用在 SELECT ... AS 里取列名,它是独立语句。

  15. random(min, max) 是 PostgreSQL 17+ 才有的重载,且区间是闭区间 [min, max],与无参 random() 的左闭右开语义相反,迁移时务必区分:

    sql
    -- PostgreSQL 17+
    SELECT random(1, 10);   -- 可能返回 1,也可能返回 10(闭区间)
    -- 旧版本等价写法(闭区间 [1, 10])
    SELECT floor(random() * 10)::int + 1;
  16. random_normal() 是 PostgreSQL 16+ 才有的函数,且它生成的是无界正态分布值,用于金额、时长等非负字段时必须夹取:

    sql
    -- 不推荐!可能生成负金额
    SELECT random_normal(200, 60);
    -- 推荐:夹取下界
    SELECT greatest(random_normal(200, 60), 0);
  17. random() 与 random_normal() 共享同一个随机数生成器状态,同一会话中交错调用会互相消耗序列;setseed() 对两者同时生效,复现实验时不要混用。

最佳实践 ​

  1. 统一封装随机整数函数,避免各处写错公式:

    sql
    CREATE OR REPLACE FUNCTION rand_int(lo INTEGER, hi INTEGER)
    RETURNS INTEGER
    LANGUAGE sql
    VOLATILE
    AS $$
      SELECT floor(random() * (hi - lo + 1))::int + lo;
    $$;
  2. 大表抽样优先用 TABLESAMPLE,并按需加 REPEATABLE (seed) 保证可复现。

  3. 随机取 N 行用「主键区间随机起点 + 顺序 LIMIT」,避免全表排序。

  4. A/B 分流用确定性哈希(hashtext / md5)而非 random(),保证同一用户分组稳定。

  5. 加权抽奖把权重落到表里(如 weight 列),用累计权重实现,避免硬编码概率。

  6. 测试数据生成前 setseed,把种子写进测试脚本注释,保证失败可复现。

  7. 造数统一用 generate_series + random(),一次 INSERT 生成全量数据,比循环插入快两个数量级。

  8. random() 相关的表达式不要建索引。VOLATILE 函数不满足索引的 IMMUTABLE 要求:

    sql
    -- 错误!ERROR: functions in index expression must be marked IMMUTABLE
    CREATE INDEX idx_users_rand ON users (random());
  9. 需要「一行只算一次随机数」时用 CTE 固化:

    sql
    WITH r AS (SELECT random() AS v)
    SELECT u.id, r.v FROM users u CROSS JOIN r;
  10. 概率配置写在表或配置表里,不要散落在 SQL 中,便于运营调整。

  11. 压测数据生成脚本要版本化,记录种子与数据规模,保证基线可比。

  12. 安全令牌一律由应用层或 pgcrypto 生成,绝不用 random()+md5 这类组合。

下一节

继续学习:数学常量与三角函数

参考链接 ​