pg_nanotime 實例集

以下每段 SQL 與輸出都是在 PostgreSQL 17.11 與 18.6 上實際執行的結果(兩版輸出完全相同),直接取自回歸測試 test/sql/examples.sql 與 test/expected/examples.out。每次 make installcheck 都會重跑,另外 make check-docs(tools/check_examples.py)與 CI 會逐行比對本文件的程式碼區塊與測試檔,不一致就失敗,所以文件與實際行為保持一致。

共同設定:SET timezone = 'UTC';。與現在時間有關的值都改用固定字面值,或只顯示計數,不顯示時間本身。

1. 交易 tick 資料與延遲統計

交易所在 exch_ts 蓋上時間,我們收到時在 recv_ts 記下時間(正式環境用 DEFAULT clock_ns8() 自動填入;這裡為了輸出固定,直接寫入時間字面值)。兩者相減就是延遲,以奈秒精確計算。

CREATE TABLE ticks (
  symbol    text              NOT NULL,
  price     numeric(12,4)     NOT NULL,
  exch_ts   nanotimestamptz8  NOT NULL,                  -- time stamped by the exchange
  recv_ts   nanotimestamptz8  NOT NULL DEFAULT clock_ns8() -- time we received it
);
INSERT INTO ticks VALUES
  ('2330', 1025.0000, '2026-10-23 01:30:00.000000100+00', '2026-10-23 01:30:00.000041350+00'),
  ('2330', 1025.5000, '2026-10-23 01:30:00.000250000+00', '2026-10-23 01:30:00.000289007+00'),
  ('2330', 1026.0000, '2026-10-23 01:30:00.001000000+00', '2026-10-23 01:30:00.001102500+00'),
  ('2317',  212.5000, '2026-10-23 01:30:00.000000500+00', '2026-10-23 01:30:00.000038900+00'),
  ('2317',  213.0000, '2026-10-23 01:30:00.000900000+00', '2026-10-23 01:30:00.000961234+00');
SELECT symbol, exch_ts, recv_ts - exch_ts AS latency FROM ticks ORDER BY exch_ts;
 symbol |             exch_ts              |      latency       
--------+----------------------------------+--------------------
 2330   | 2026-10-23 01:30:00.000000100+00 | 00:00:00.000041250
 2317   | 2026-10-23 01:30:00.000000500+00 | 00:00:00.000038400
 2330   | 2026-10-23 01:30:00.000250000+00 | 00:00:00.000039007
 2317   | 2026-10-23 01:30:00.000900000+00 | 00:00:00.000061234
 2330   | 2026-10-23 01:30:00.001000000+00 | 00:00:00.000102500
(5 rows)

SELECT symbol,
       count(*)                                                AS n,
       min((recv_ts - exch_ts)::bigint)                        AS min_ns,
       round(avg((recv_ts - exch_ts)::bigint))                 AS avg_ns,
       percentile_disc(0.5) WITHIN GROUP (ORDER BY (recv_ts - exch_ts)::bigint) AS p50_ns,
       max((recv_ts - exch_ts)::bigint)                        AS max_ns
FROM ticks GROUP BY symbol ORDER BY symbol;
 symbol | n | min_ns | avg_ns | p50_ns | max_ns 
--------+---+--------+--------+--------+--------
 2317   | 2 |  38400 |  49817 |  38400 |  61234
 2330   | 3 |  39007 |  60919 |  41250 | 102500
(2 rows)

-- latency of anything slower than 50 microseconds
SELECT symbol, price, recv_ts - exch_ts AS latency
FROM ticks WHERE recv_ts - exch_ts > '50 microseconds'::nanointerval ORDER BY exch_ts;
 symbol |   price   |      latency       
--------+-----------+--------------------
 2317   |  213.0000 | 00:00:00.000061234
 2330   | 1026.0000 | 00:00:00.000102500
(2 rows)

2. 撮合引擎:先到先處理(FIFO)

clock_ns8_global() 在所有連線之間保證嚴格遞增、不重複,可以直接當主鍵與排隊順序。需要 shared_preload_libraries = 'pg_nanotime'。時間值每次執行都不同,所以這裡只顯示計數與排序結果,不顯示時間本身。

CREATE TABLE orders (
  seq_ts  nanotimestamptz8 NOT NULL DEFAULT clock_ns8_global() PRIMARY KEY,
  side    char(1)          NOT NULL,
  price   numeric(12,4)    NOT NULL,
  qty     int              NOT NULL
);
-- 10000 orders in one statement: every default is a distinct, increasing time
INSERT INTO orders(side, price, qty)
SELECT CASE WHEN g % 2 = 0 THEN 'B' ELSE 'S' END, 100 + g % 7, g % 10 + 1
FROM generate_series(1, 10000) g;
SELECT count(*) AS orders, count(DISTINCT seq_ts) AS distinct_ts FROM orders;
 orders | distinct_ts 
--------+-------------
  10000 |       10000
(1 row)

SELECT count(*) AS not_increasing
FROM (SELECT seq_ts, lag(seq_ts) OVER (ORDER BY ctid) AS prev FROM orders) s
WHERE seq_ts <= prev;
 not_increasing 
----------------
              0
(1 row)

-- FIFO: the best-priced buy orders, oldest first
SELECT side, price, qty FROM orders
WHERE side = 'B' ORDER BY price DESC, seq_ts LIMIT 3;
 side |  price   | qty 
------+----------+-----
 B    | 106.0000 |   7
 B    | 106.0000 |   1
 B    | 106.0000 |   5
(3 rows)

3. 網路封包:匯入 bigint 奈秒

pcap、PTP 等工具常輸出「1970 年起的奈秒整數」。::nanotimestamptz8 直接轉換(不需運算),也能轉回 bigint 匯出。

CREATE TABLE packets_raw (ts_ns bigint, src inet, len int);
INSERT INTO packets_raw VALUES
  (1792719000000000000, '10.0.0.1', 64),
  (1792719000000001250, '10.0.0.2', 1500),
  (1792719000000001300, '10.0.0.1', 64),
  (1792719000000250000, '10.0.0.3', 576);
CREATE TABLE packets AS
  SELECT ts_ns::nanotimestamptz8 AS ts, src, len FROM packets_raw;
SELECT ts, src, len, ts - lag(ts) OVER (ORDER BY ts) AS gap FROM packets ORDER BY ts;
                ts                |   src    | len  |        gap         
----------------------------------+----------+------+--------------------
 2026-10-23 01:30:00.000000000+00 | 10.0.0.1 |   64 | 
 2026-10-23 01:30:00.000001250+00 | 10.0.0.2 | 1500 | 00:00:00.000001250
 2026-10-23 01:30:00.000001300+00 | 10.0.0.1 |   64 | 00:00:00.000000050
 2026-10-23 01:30:00.000250000+00 | 10.0.0.3 |  576 | 00:00:00.000248700
(4 rows)

-- and back to bigint for export
SELECT ts::bigint AS ts_ns FROM packets ORDER BY ts LIMIT 1;
        ts_ns        
---------------------
 1792719000000000000
(1 row)

4. IoT 感測器:每秒分桶統計

每 250 ms 一筆讀數,用 time_bucket_ns 依 1 秒分桶。

CREATE TABLE readings (sensor int, ts nanotimestamptz, value double precision);
INSERT INTO readings
SELECT s, '2026-10-23 00:00:00+00'::nanotimestamptz + (g * 250000000::bigint)::nanointerval,
       s * 10 + g % 4
FROM generate_series(1, 2) s, generate_series(0, 11) g;
SELECT sensor, time_bucket_ns('1 second'::nanointerval, ts) AS bucket,
       count(*) AS n, avg(value) AS avg_value
FROM readings GROUP BY 1, 2 ORDER BY 1, 2;
 sensor |              bucket              | n | avg_value 
--------+----------------------------------+---+-----------
      1 | 2026-10-23 00:00:00.000000000+00 | 4 |      11.5
      1 | 2026-10-23 00:00:01.000000000+00 | 4 |      11.5
      1 | 2026-10-23 00:00:02.000000000+00 | 4 |      11.5
      2 | 2026-10-23 00:00:00.000000000+00 | 4 |      21.5
      2 | 2026-10-23 00:00:01.000000000+00 | 4 |      21.5
      2 | 2026-10-23 00:00:02.000000000+00 | 4 |      21.5
(6 rows)

5. 從 timestamp9 遷移

這裡用 bigint domain 模擬 timestamp9(數值相同:1970 年起的奈秒),讓測試不依賴 timestamp9;真實 timestamp9 欄位的呼叫方式完全相同,實測紀錄見 TESTING.md。先 dry_run 看要執行的 SQL,再正式執行。

-- timestamp9 here; with the real extension the same calls work.
CREATE DOMAIN timestamp9 AS bigint;
CREATE TABLE events (id int PRIMARY KEY, t timestamp9 NOT NULL);
INSERT INTO events VALUES (1, 1792719000000000001), (2, -1), (3, 1792719000123456789);
CREATE INDEX events_t ON events(t);
CALL nanotime_migrate_column('events', 't', dry_run => true);
NOTICE:  index events_t will be rebuilt
NOTICE:  ALTER TABLE events ALTER COLUMN t TYPE nanotimestamptz USING (t)::bigint::nanotimestamptz
CALL nanotime_migrate_column('events', 't');
NOTICE:  index events_t will be rebuilt
SELECT id, t FROM events ORDER BY t;
 id |                t                 
----+----------------------------------
  2 | 1969-12-31 23:59:59.999999999+00
  1 | 2026-10-23 01:30:00.000000001+00
  3 | 2026-10-23 01:30:00.123456789+00
(3 rows)

SELECT pg_typeof(t) FROM events LIMIT 1;
    pg_typeof    
-----------------
 nanotimestamptz
(1 row)

6. 交易內固定時間與即時時鐘

now_ns() 在同一交易內不變;clock_ns_monotonic() 每次呼叫都會前進。

BEGIN;
SELECT now_ns() = now_ns() AS same_in_txn, clock_ns_monotonic() < clock_ns_monotonic() AS clock_moves;
 same_in_txn | clock_moves 
-------------+-------------
 t           | t
(1 row)

COMMIT;