# pg_nanotime PostgreSQL 奈秒精度時間戳擴充套件,靈感來自 [timestamp9](https://github.com/optiver/timestamp9),並在此基礎上進一步發展。授權:PostgreSQL License。 > 狀態:**v0.3.0 已釋出(2026-10-26;範圍型別、TimescaleDB 一鍵設定)**。原始碼:[https://github.com/bunko666-cloud/pg_nanotime](https://github.com/bunko666-cloud/pg_nanotime)。[![CI](https://github.com/bunko666-cloud/pg_nanotime/actions/workflows/ci.yml/badge.svg)](https://github.com/bunko666-cloud/pg_nanotime/actions/workflows/ci.yml) [English summary](#english) > > **只支援 PostgreSQL 17 與 18**(已在 17.11、18.6 上建置並通過全部回歸測試)。PG 16 以下會在建置時直接報錯:Makefile 會檢查 `pg_config --version`,原始碼也有 `#error`。程式中沒有任何舊版相容碼。 > > 每個物件由哪些測試涵蓋、已知缺口與改動時要重跑哪些測試,見 [COVERAGE.md](COVERAGE.md)。 ## 使用原則(重要) 奈秒欄位請**一律**使用 `nanotimestamptz`(或 `nanotimestamptz8`),並搭配 `now_ns()` / `statement_ns()` / `clock_ns()` 產生時間。**不要**和 `now()` / `timestamptz` 混用: - `now_ns()` 是交易中**第一個查詢開始執行時**由本擴充套件讀取的時鐘,而 `now()` 是 PostgreSQL 在交易開始時讀取的另一個時鐘值。兩者通常相差幾微秒,`now_ns()` 比較晚。如果同一筆資料一欄用 `now()`、另一欄用 `now_ns()`,或拿 `now()` 去和 `now_ns()` 寫入的值比較,就會出現幾微秒的不一致。 - `statement_ns()` 同理,是陳述式開始時讀取的值,同一陳述式內不變。 - `clock_ns()` 的語意和 `clock_timestamp()` 相同:每次呼叫都即時讀取牆鐘(`CLOCK_REALTIME`),只是精度到奈秒。 - **這是使用慣例,擴充套件不會阻止混用。**`timestamptz → nanotimestamptz` 是 IMPLICIT 轉型,所以 `INSERT INTO t(ts) VALUES (now())`、`clock_ns() - now()`、`WHERE ts > now()` 都能執行、不會報錯,但比較的是兩個不同來源的時鐘(而且 `now()` 只有微秒精度),結果可能差幾微秒。請自行在程式碼審查或欄位 DEFAULT 中統一使用 pg_nanotime 的函式。 ## 型別 `nanotimestamptz` | 項目 | 說明 | |---|---| | 儲存 | 16 bytes:`{ int64 sec; int64 nsec; }`,對齊 `double`,`STORAGE plain` | | 紀元 | Unix 紀元 1970-01-01 00:00:00 UTC;`sec` 為向下取整(floor)的秒數 | | `nsec` | 永遠正規化為 0..999999999(1969-12-31 23:59:59.999999999 = `{-1, 999999999}`) | | 範圍 | 與 `timestamptz` 相同:4714-11-24 00:00:00 BC(PG 文件稱「4713 BC」,為儒略曆寫法)到 294276-12-31 23:59:59.999999999 | | 閏秒 | 不處理(與 PostgreSQL 一致);CLOCK_TAI 時鐘來源見 Phase 3。閏秒將於 2035 年前停用,相關決定見 [ROADMAP.md](ROADMAP.md) | ### 文字輸入/輸出 - 輸入:接受 `timestamptz` 能接受的任何格式,秒數後可帶 1–9 位小數;第 10 位以後只允許 `0`,否則報錯(不靜默截斷)。時區、`BC` 的解析完全交給 PostgreSQL 本身。 - 輸出:固定 ISO 8601 樣式 `YYYY-MM-DD HH:MI:SS.fffffffff±TZ`,永遠 9 位小數,依 session `TimeZone` 顯示偏移(與 `timestamptz` 在 `DateStyle=ISO` 時相同)。**不跟隨 DateStyle**。 - 負值輸出無歧義:`(-1)::bigint::nanotimestamptz` → `1969-12-31 23:59:59.999999999+00`。 - `infinity` / `-infinity`:v0.1 不支援,會報錯。 ### 二進位 I/O `send`/`recv`:兩個 big-endian int64(sec、nsec)。`recv` 會檢查 nsec 範圍與時間範圍。 ## 精簡型別 `nanotimestamptz8` | 項目 | 說明 | |---|---| | 儲存 | 8 bytes,傳值(pass by value),int64 Unix 奈秒,與 `bigint` 的二進位格式相同 | | 範圍 | 1677-09-21 00:12:43.145224192 到 2262-04-11 23:47:16.854775807 UTC,沒有 1970 下限;超出範圍一律報錯,不會溢位繞回 | | I/O | 文字輸入/輸出的程式碼與 16 B 型別共用;binary 格式與 int8 相同 | | 索引 | B-tree、hash、BRIN minmax(比較直接使用 PG 內建 int8 函式) | | 轉型 | 轉成 `nanotimestamptz` 精確且為 IMPLICIT(所以兩型別可以直接比較);`nanotimestamptz → 8` 為 ASSIGNMENT,超出範圍報錯;與 `timestamptz` 雙向為 ASSIGNMENT,轉成 timestamptz 時 floor;與 `bigint` 雙向為明確轉型,`WITHOUT FUNCTION`,零成本 | | 時鐘 | `clock_ns8()`、`now_ns8()`、`statement_ns8()`,與 16 B 版本是同一次時鐘讀取 | ### TimescaleDB(2.31.0 實測) - 可以直接作為 hypertable 的時間維度,不需要分區函數:`create_hypertable('t', by_range('ts', 86400000000000))`。chunk 排除、壓縮、`drop_chunks(older_than => '...'::nanotimestamptz8)` 都正常,預設索引為 `(ts DESC)`。 - 若建立時指定了 `partition_func => 'nanotime_part8'`(要用 `set_integer_now_func` 或保留策略時需要),`drop_chunks` 的 `older_than` 必須是 bigint:寫 `'...'::nanotimestamptz8::bigint`;直接給 `nanotimestamptz8` 會報 `invalid time argument type "nanotimestamptz8"`。此時 TimescaleDB 的預設索引建在 `nanotime_part8(ts)` 運算式上,直接用 `ts` 查詢用不到它(chunk 排除仍有效,但 chunk 內是 seq scan),請另外建立 `CREATE INDEX ON t (ts DESC);`。 - 原生維度**不能**使用 `set_integer_now_func` 與連續聚合:TimescaleDB 不把它當成整數維度。`add_retention_policy` 雖然建得起來,執行時卻報 `interval field value out of range`(2.31.0 實測),要用保留策略請設 `partition_func => 'nanotime_part8'`,或直接用 0.3.0 的 `nanotime_timescale_setup()`。`time_bucket` 也沒有這個型別的版本,請用 `time_bucket(bigint, ts::bigint)::nanotimestamptz8` 查詢。需要連續聚合時,請把時間欄位存成 `bigint`。 ## 運算子與索引 - `< <= = <> >= >`、`min()`/`max()`、`greatest`/`least`。 - B-tree、hash、BRIN minmax 運算子類別(都是預設);`=` 支援 hash join 與 merge join。 | 型別 | B-tree | hash | BRIN minmax | GiST / GIN | |---|---|---|---|---| | `nanotimestamptz` | ✓ | ✓ | ✓ | ✗ | | `nanotimestamptz8` | ✓ | ✓ | ✓ | ✗ | | `nanotimestamp` | ✓ | ✓ | ✓ | ✗ | | `nanointerval` | ✓ | ✓ | ✗ | ✗ | | `nanotstzrange`/`nanotsrange`(0.3.0) | ✓(內建 range_ops) | ✓(內建) | ✗ | GiST ✓(內建 range_ops) | - 不支援 BRIN minmax-multi 與 bloom;int8 的 opclass(如 `int8_minmax_multi_ops`)也不接受 `nanotimestamptz8`。 - 單一時間點的 GiST/GIN 沒有運算子類別(安裝 `btree_gist` 也一樣)。需要「時段不可重疊」時,請用 0.3.0 新增的範圍型別 `nanotstzrange`/`nanotsrange`,它們使用 PostgreSQL 內建的 GiST `range_ops`,可以搭配 `EXCLUDE USING gist`,見[範圍型別](#範圍型別-nanotstzrangenanotsrange030)。 - 參考(PG 18.6,100 萬列依時間寫入的 `nanotimestamptz8`):B-tree 21 MiB、hash 32 MiB、BRIN 24 KiB。 ## 型別轉換 | 轉換 | 語意 | 情境 | |---|---|---| | `timestamptz → nanotimestamptz` | 精確 | IMPLICIT | | `nanotimestamptz → timestamptz` | 向下取整到微秒(1970 前亦為 floor) | ASSIGNMENT | | `bigint → nanotimestamptz` | bigint = 1970 起算的奈秒數 | 僅 EXPLICIT | | `nanotimestamptz → bigint` | 超出 1677-09-21 00:12:43.145224192 ~ 2262-04-11 23:47:16.854775807 時報錯 | 僅 EXPLICIT | 所有超出範圍的情況一律報錯,不會溢位繞回(wraparound)。 ## 時鐘函式 | 函式 | 易變性 | 說明 | |---|---|---| | `clock_ns()` | VOLATILE | 每次呼叫讀 `clock_gettime(CLOCK_REALTIME)` | | `now_ns()` | STABLE | 交易開始時間(奈秒)。在該交易第一次 executor 啟動時讀取時鐘並快取 | | `statement_ns()` | STABLE | 目前陳述式開始時間(奈秒),同一陳述式內不變 | 注意:`now_ns()` 是由擴充套件在 `ExecutorStart_hook` 讀取的時鐘,**不是** PostgreSQL `now()` 的同一次讀取,因此通常比 `now()` 晚數微秒(`now_ns()::timestamptz >= now()`)。如果函式庫在陳述式開始後才首次載入,第一次呼叫時才讀時鐘。建議在 `postgresql.conf` 設定 `shared_preload_libraries = 'pg_nanotime'` 或 `session_preload_libraries`,讓時間更貼近交易開始。 ```sql CREATE TABLE events ( id bigserial, ts nanotimestamptz NOT NULL DEFAULT clock_ns(), tx nanotimestamptz NOT NULL DEFAULT now_ns() ); ``` ## 安裝 需要 PostgreSQL 17 或 18 的開發套件(Debian/Ubuntu 為 `postgresql-server-dev-17` 或 `postgresql-server-dev-18`)與 C 編譯器。 ```sh git clone https://github.com/bunko666-cloud/pg_nanotime pg_nanotime && cd pg_nanotime make PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config sudo make install PG_CONFIG=/usr/lib/postgresql/18/bin/pg_config # 或從 PGXN 安裝:pgxn install pg_nanotime(發佈後) ``` 若要使用 `clock_ns_global()`,或想讓 `now_ns()` 更貼近交易開始時間,請在 `postgresql.conf` 加上以下設定,然後重新啟動: ``` shared_preload_libraries = 'pg_nanotime' # 已用 TimescaleDB 時:'timescaledb, pg_nanotime' ``` ## 快速上手 ```sql CREATE EXTENSION pg_nanotime; CREATE TABLE trades ( id bigserial PRIMARY KEY, t nanotimestamptz NOT NULL DEFAULT clock_ns(), -- 完整範圍,16 B t8 nanotimestamptz8 NOT NULL DEFAULT clock_ns8(), -- 1677–2262,8 B seq nanotimestamptz8 NOT NULL DEFAULT clock_ns8_global() -- 跨連線嚴格遞增(需要 preload) ); INSERT INTO trades DEFAULT VALUES; SELECT t, t - lag(t) OVER (ORDER BY t) AS gap FROM trades; SELECT time_bucket_ns('1 second', t), count(*) FROM trades GROUP BY 1; -- 從 timestamp9 遷移: CALL nanotime_migrate_column('old_table', 'ts', new_default => 'clock_ns()'); ``` ## 建置與測試 ```sh make installcheck PGHOST=/tmp PGPORT=5432 PGUSER=postgres ``` - 加上 `WITH_TIMESCALEDB=1` 會多跑 `timescale` 測試。 - 有沒有 preload 都能跑:`global` 測試準備了兩份預期結果。 - `make dist` 會從 HEAD 產生 PGXN 用的 zip。 ## 文件 - [docs/TESTING.md](TESTING.md):測試報告,包含環境、回歸測試、timestamp9 行為、TimescaleDB 相容性、效能、遷移與並行測試 - [docs/TIMESCALEDB.md](TIMESCALEDB.md):TimescaleDB 使用範例 - [bench/](../bench/README.md):重現測試用的腳本 - [CHANGELOG.md](../CHANGELOG.md)、[CONTRIBUTING.md](../CONTRIBUTING.md) ## 尚未完成 - `infinity`,以及依 DateStyle 輸出。 - TimescaleDB 連續聚合:需要向上游提案。 ## Phase 2:nanointerval、算術、分桶 ### nanointerval - 只存固定奈秒數(沒有月、日欄位),16 bytes `{int64 sec, int64 nsec}`。 - **為什麼不用 int64**:int64 奈秒只涵蓋約 ±292 年,但 `nanotimestamptz` 涵蓋約 30 萬年,任兩個值相減都必須放得下。16B 的範圍是 ±(max − min) ≈ ±9.4e21 ns,剛好容納所有 ts − ts 的差值,超出時嚴格報錯 `nanointerval out of range`。 - 輸出:`1 day 02:03:04.123456789`、`-1 day -02:00:00.000000000`、`-14 days`,小數固定 9 位。 - 輸入:單位字(ns/us/ms/s/min/h/d/w 及全名)、`HH:MM[:SS.f]`、`@`、`ago`、純數字(秒)、ISO 8601 `P..T..`。month/year 會報錯並提示改用 interval;比 1 ns 更細也會報錯。 - 運算子:比較、B-tree、hash、一元 `-`、`+`、`-`、`* / int`(/ 四捨五入,遠離 0)、`* / numeric`。`smallint` 需要先轉型(不然會有 `operator is not unique`)。 - 轉型:`interval ↔ nanointerval` 是 ASSIGNMENT 轉型。從 interval 轉入時,有月份或 infinity 會報錯,**1 day 當作 24 小時**。轉成 interval 時會向下取整到 µs。`bigint ↔ nanointerval`(奈秒)只能明確轉型。 - `nanointerval_ns(x) → numeric`、`nanointerval_from_ns(numeric | bigint | double precision)`:`bigint` 與 `numeric` 版本是精確的(整數參數會選到 bigint 版本;`bigint` 與 `double precision` 版本 0.2.0 起才有,0.1.0 的整數參數走 numeric),先四捨五入到整數奈秒(遠離 0),超出範圍會報錯。**沒有 `numeric → nanointerval` 轉型**,`1500::numeric::nanointerval` 會報 `cannot cast type numeric to nanointerval`;請寫 `nanointerval_from_ns(1500)`(→ `00:00:00.000001500`),整數奈秒也可以寫 `1500::bigint::nanointerval`。 - `double precision` 版本(0.2.0)同樣四捨五入(遠離 0)。double 只有 53 位元尾數,超過 2^53 ns(約 104 天)就無法表示每個整數奈秒,例如 `9007199254740993::float8` 會變成 …992;需要精確時請傳 bigint 或 numeric。 - NaN 報 `cannot convert NaN to nanointerval`,±Infinity 報 `cannot convert infinity to nanointerval`(numeric 與 double precision 都一樣;0.1.0 對 numeric 報的是 `nanointerval out of range`)。有限但超出範圍的值仍報 `nanointerval out of range`。 - `abs(nanointerval)`(0.2.0):nanointerval 的範圍是對稱的(±NI_MAX),所以最小值也能取負,`abs` 與一元 `-` 都不會溢位。 - `generate_series(start, stop, step nanointerval)`(0.2.0),`start`/`stop` 可為 `nanotimestamptz`、`nanotimestamptz8`、`nanotimestamp`:語意與 PostgreSQL 的 timestamptz 版本相同——包含 `stop`;step 為 0 報 `step size cannot equal zero`;負的 step 往回數;方向不對時回傳空集合;任一參數 NULL 時回傳空集合。內部用 int128 計算,輸出值一定介於 start 與 stop 之間,所以在型別範圍邊緣(例如 nanotimestamptz8 的 2262-04-11)不會繞回也不會報錯。附有 planner support 函式:三個參數都是常數時,列數估計就是精確的序列長度(例如 1 小時/1 分鐘 → 61 列),否則用預設估計。step 是固定奈秒數,不處理月份等日曆單位(那是 `interval` 的事)。 - 聚合(0.2.0 起):`min`、`max`、`sum`、`avg`。`sum`/`avg` 以 int128 精確累加奈秒並計數,不會有浮點誤差;結果超出範圍時報錯 `nanointerval out of range`,不會繞回。`avg` 四捨五入到最近的奈秒,剛好一半時遠離 0(與 `nanointerval / bigint` 一致:1 ns 與 2 ns 平均是 2 ns,-1 ns 與 -2 ns 是 -2 ns)。NULL 會被忽略,沒有資料列或全為 NULL 時回傳 NULL。支援平行查詢(combine/serialize)與視窗函式的移動聚合(inverse transition)。 ### 算術(兩種時間型別都有,溢位都會報錯) - `ts − ts → nanointerval` - `ts ± nanointerval`:固定奈秒 - `ts ± interval`:依 session 時區做日曆運算(與 timestamptz 相同,DST 下 +1 day 會保留牆上時間),保留 µs 以下的位數。因為受時區影響,所以是 STABLE。 - ⚠️ **字面值陷阱**:`ts + '1 day'` 這類未標型別的字串會解析成內建的 **interval**(日曆語意、µs 精度)。需要奈秒精度或固定長度時,請寫 `'...'::nanointerval`。`ts − '2024-01-01'` 會解析成 ts − ts。 ### 分桶與相關函式 - `time_bucket_ns(width nanointerval, ts [, origin])`:兩種型別都有,回傳與輸入相同的型別。預設 origin 是 1970-01-01 UTC,向下取整(1970 年以前也正確)。width ≤ 0 會報錯。**若桶起點低於型別最小值,會夾到最小值**(例如 nt8 在 1677-09-21 那一天)。 - `date_trunc(text, ts [, zone])`:µs 以上的單位交給 PG 的 timestamptz_trunc;`'nanosecond'` 則原值回傳。 - `extract_ns(text, ts|nanointerval) → numeric`(`EXTRACT(... FROM ...)` 語法在 PG 中寫死,無法擴充):epoch 精確到 9 位小數;nanosecond、microsecond、millisecond、second 都含奈秒,而且和 PG 的 `EXTRACT(microsecond ...)` 一樣**包含整數秒**:`extract_ns('nanosecond', '...01.000000500+00')` 回傳 `1000000500`(1 秒 + 500 ns),`'microsecond'` 回傳 `1000000.500`;只要秒以下的部分請用 `extract_ns('nanosecond', ts) % 1000000000`;其他欄位交給 PG 處理。nanointerval 支援 epoch、day、hour、minute 與秒以下的欄位。 ### nanotimestamp(不含時區) - 16 bytes `{int64 sec, int64 nsec}`,與 nanotimestamptz 共用程式碼。sec 是 1970-01-01 00:00:00 起算的牆上時間秒數(不含時區),語意與 PG `timestamp` 相同。範圍等同 timestamp。 - text I/O 會忽略輸入中的時區(`'...+05'` 直接取牆上時間),輸出沒有時區後綴;IMMUTABLE。 - 比較、min/max、B-tree、hash、BRIN minmax。 - 轉型(context 與核心的 timestamp/timestamptz 相同): - `timestamp → nanotimestamp` IMPLICIT;反向是 ASSIGNMENT,floor 到 µs。 - `date → nanotimestamp` IMPLICIT;反向是 ASSIGNMENT。`date ↔ nanotimestamptz` 也有,都是 ASSIGNMENT。 - `nanotimestamp → nanotimestamptz` IMPLICIT;反向是 ASSIGNMENT。兩者都依 session 時區換算(DST 的 gap 和 overlap 處理和核心相同),µs 以下的位數保留。 - `bigint ↔ nanotimestamp` 只能明確轉型(牆上時間 ns)。 - `AT TIME ZONE`:這個語法在 PG 中寫死為 `pg_catalog.timezone`,擴充套件無法支援。請改用 `timezone(zone, nanotimestamp) → nanotimestamptz` 與 `timezone(zone, nanotimestamptz) → nanotimestamp`,結果與核心一致。 - 算術:`ts − ts`、`ts ± nanointerval`、`ts ± interval`(純日曆運算,不受時區與 DST 影響,IMMUTABLE)、`time_bucket_ns`、`date_trunc`(沒有 zone 參數)、`extract_ns`。 **為什麼沒有 8 B 不含時區版本**:8 B 型別存在的理由,是 TimescaleDB 維度和 bigint 二進位相容,加上大量 tick 資料。這些資料都是絕對時刻,應該存 `nanotimestamptz8`。不含時區的牆上時間多半是排程、本地事件,量小,而且需要 1677–2262 以外的範圍。每多一個型別,轉型與運算子的組合就會成倍增加,候選函式也更容易產生歧義。真的需要時,可以用 `nanotimestamptz8` 搭配 `timezone('UTC', …)`。 ## 遷移工具 - `nanotime_migration_expr(src regtype, target regtype, col name) → text`:產生 `ALTER TABLE ... USING` 的運算式。 - timestamp9 經由它自己的二進位 bigint 轉型 `col::bigint::nanotimestamptz`。pg_nanotime 不依賴 timestamp9,只是欄位存在時 timestamp9 必須已安裝。 - bigint ns 直接轉型,不做任何換算(`col::nanotimestamptz` 或 `col::nanotimestamptz8`,依目標型別)。 - timestamptz、timestamp、date 是精確轉換(µs × 1000);跨越有無時區時,依 session 時區。 - `CALL nanotime_migrate_column(tbl, col, target DEFAULT 'nanotimestamptz', new_default DEFAULT NULL, dry_run DEFAULT false)`: - 預設目標一律是 16 B `nanotimestamptz`,來源是 bigint 時也一樣;要轉成 8 B 請明確傳入 `'nanotimestamptz8'`,例如 `CALL nanotime_migrate_column('t', 'c', 'nanotimestamptz8');`。 - 用一個 `ALTER TABLE ... TYPE ... USING` 完成。PG 會在同一個陳述式中,用新型別的預設 opclass 重建該欄位的 B-tree、hash、BRIN、unique 索引,失敗時整個回滾。 - 舊的預設值如果無法轉換(例如 timestamp9 常數),會報錯並提示用 `new_default => 'clock_ns()'`。 - 有 view 或 rule 依賴該欄位時,ALTER 會失敗(PG 的限制)。 - `dry_run` 只印出 SQL 和會重建的索引。 - 手動寫法:`ALTER TABLE t ALTER COLUMN c TYPE nanotimestamptz USING c::bigint::nanotimestamptz;`(timestamp9),或 `USING c::nanotimestamptz8`(bigint ns)。 - **timestamp9 繞回損毀**:timestamp9 會把超出範圍的輸入靜默繞回,例如 1500-01-01 變成 2084-07-20 23:34:33.709551616。繞回後仍是合法時刻,**無法可靠偵測**。`nanotime_suspect_count(tbl, col, lo, hi)` 只能統計落在你預期時間窗 [lo, hi) 之外的列數,預設窗是 1990–2100。窗要依照資料實際的時間跨度來設,越窄越有用。 - 實測(PG 18.6、timestamp9 1.4.0,2026-10-23): - 20 萬 + 4 列,含 1677/2262 兩端、1969 與 1500 的繞回值,3 個索引,約 0.5 秒完成。 - 200004/200004 列的 ns 值與文字輸出完全相同。 - 預設窗只抓到 3 列(1677、1969、2262),抓不到 2084 的繞回值,正好說明它的限制。 - 回歸測試用一個名為 timestamp9 的 bigint domain 模擬,因為 PG 17 上沒有安裝 timestamp9。 ## Phase 3:嚴格遞增時鐘、時鐘來源、TimescaleDB 輔助 ### 嚴格遞增時鐘 | 函式 | 保證 | 需求 | |---|---|---| | `clock_ns_monotonic()` / `clock_ns8_monotonic()` | 同一連線內嚴格遞增:時鐘沒有前進時取上一個值 + 1 ns。不加鎖。16 B 和 8 B 版本共用同一個計數器 | 無 | | `clock_ns_global()` / `clock_ns8_global()` | 跨所有連線嚴格遞增且不重複:在共享記憶體上用 atomic CAS 取 `max(now, last + 1)` | `shared_preload_libraries = 'pg_nanotime'`,否則會報出清楚的錯誤 | 注意事項: - 保證的是「呼叫順序」,**不是** commit 順序。先取到時間的交易可能較晚 commit。 - global 的計數器存在共享記憶體,伺服器重啟後會重來。 - 多個連線同時大量呼叫時,global 的總速率只有一般時鐘的約 14–28%。實測數字見 [docs/TESTING.md](TESTING.md) §8。 ### 時鐘倒退與監控 `SELECT * FROM nanotime_clock_status();` 回傳以下欄位: - 目前的時鐘來源、核心 TAI 偏移(秒)、是否已 preload。 - global:最後發出的值、`global_lead_ns`(領先實際時鐘的奈秒數,持續為正表示時鐘倒退過或呼叫非常密集)、呼叫次數、`bumps`(時鐘沒前進、改用 +1 ns 的次數)、倒退次數與最大倒退幅度。 - 本連線:同樣一組數值。 倒退的判定方式:同一連線讀到的時鐘值比上一次小。切換時鐘來源不算倒退。`nanotime_reset_stats()` 用來歸零計數,預設只有 superuser 能執行。時鐘倒退時,monotonic 與 global 會持續 +1 ns,直到實際時鐘追上;一般的 `clock_ns()` 則會跟著時鐘倒退。 ### `nanotime.clock_source = realtime | tai` - `realtime`(預設):`CLOCK_REALTIME`,也就是 UTC。 - `tai`:`CLOCK_TAI`。**Linux 預設的 TAI 偏移是 0**,這時讀到的值和 realtime 相同。偏移要由 chrony(`leapsectz right/UTC`)或 ptp4l/phc2sys 設定,目前約為 37 秒,可以從 `nanotime_clock_status().tai_offset_s` 確認。 - 用 tai 寫入的值是 TAI 時間,但輸出仍標示為 UTC 格式,與 realtime 的值相差偏移量。請不要在同一個欄位混用兩種來源。 - 這個設定會影響所有時鐘函式,可以在每個連線各自設定。 `nanotime.debug_clock_offset_us` 只供測試使用(限 superuser),會在所有時鐘讀值上加上一個偏移,用來模擬時鐘倒退。 ### TimescaleDB 輔助 `ns(nanointerval)`、`nanotime_now_int8()`、`nanotime_part()`、`nanotime_part_notz()`、`nanotime_part8()`。用法範例見 **[docs/TIMESCALEDB.md](TIMESCALEDB.md)**。 0.3.0 新增兩個一鍵輔助函式: | 函式 | 做什麼 | |---|---| | `nanotime_timescale_setup(tbl regclass, col name, chunk nanointerval DEFAULT '1 day', migrate_data bool DEFAULT false) → text` | 依欄位型別選 `nanotime_part`/`nanotime_part_notz`/`nanotime_part8`,以 `by_range(col, chunk::bigint, partition_func)` 和 `create_default_indexes => false` 呼叫 `create_hypertable`,設定 `set_integer_now_func(tbl, 'nanotime_now_int8')`,最後在沒有等效索引時建立 `(col DESC)`;回傳一行摘要 | | `nanotime_drop_chunks(tbl regclass, older_than nanotimestamptz DEFAULT NULL, newer_than nanotimestamptz DEFAULT NULL) → SETOF text` | 依 hypertable 時間維度的分區函數把邊界換成 bigint(與分區函數相同的飽和映射),原生 8 B 維度則轉成 `nanotimestamptz8`,再呼叫 `drop_chunks` | 設計決定: - **不依賴 TimescaleDB**:兩個函式都是 PL/pgSQL,所有 TimescaleDB 物件都用動態 SQL 呼叫,並以 `pg_extension` 查出 timescaledb 與 pg_nanotime 所在的 schema 再加上限定名稱(pg_nanotime 可重新定位,`search_path` 不含它的 schema 也能用)。沒有安裝 TimescaleDB 時報 `TimescaleDB is not installed in this database`(SQLSTATE 55000)並附 HINT。 - **只支援 `by_range()` 介面**(TimescaleDB 2.13 起)。支援 PG 17/18 的 TimescaleDB 都有這個介面,所以不寫舊介面的分支;沒有時報 `feature_not_supported`。 - **8 B 型別也用分區函數** `nanotime_part8`:原生維度不能設 integer now,保留策略會在執行時失敗;統一用分區函數讓三種型別行為一致。 - **等效索引**:任何有效、非部分、第一個欄位是該時間欄位的 B-tree 索引(ASC 或 DESC)都能服務 `WHERE col ...` 與 `ORDER BY col DESC LIMIT n`,有就沿用。 - **`nanotime_drop_chunks` 的範圍**:只接受 `nanotimestamptz`(不提供 `nanotimestamp`/`nanointerval` 多載,避免不帶型別的字面值出現多載歧義);`older_than` 換算後是 int64 最小值時回傳空集合。讀取時間維度用到 TimescaleDB 內部目錄 `_timescaledb_catalog.dimension`(公開的 `timescaledb_information.dimensions` 沒有分區函數欄位)。 ## 範圍型別 `nanotstzrange`/`nanotsrange`(0.3.0) > 0.3.0 於 2026-10-26 釋出(見 CHANGELOG 的 [0.3.0])。從 0.2.0 升級:`ALTER EXTENSION pg_nanotime UPDATE TO '0.3.0';` 「這段時間有沒有和別人重疊」是很常見的問題:會議室、實驗室儀器、GPU、直播時段都不能重複預約。PostgreSQL 內建的 `tstzrange` 只到微秒,pg_nanotime 0.3.0 用 PostgreSQL 的 `CREATE TYPE ... AS RANGE` 機制,提供奈秒精度的範圍型別: | 型別 | 子型別 | 自動建立的 multirange | 對應的內建型別 | |---|---|---|---| | `nanotstzrange` | `nanotimestamptz` | `nanotstzmultirange` | `tstzrange`/`tstzmultirange` | | `nanotsrange` | `nanotimestamp` | `nanotsmultirange` | `tsrange`/`tsmultirange` | ```sql CREATE EXTENSION btree_gist; -- 讓 text 等一般欄位也能放進 GiST 排他約束 CREATE TABLE booking ( room text, during nanotstzrange, EXCLUDE USING gist (room WITH =, during WITH &&) -- 同一間不能重疊 ); ``` ### 可以用什麼 全部是 PostgreSQL 內建的範圍機制,沒有自訂新的運算子(ROADMAP 的範圍外決定): - 建構函式:`nanotstzrange(lower, upper [, '[)'])`、`nanotsrange(...)`、`nanotstzmultirange(range, ...)`、`nanotsmultirange(...)`。`nanotimestamptz8` 與 `timestamptz` 會經由隱含轉型直接傳入 `nanotstzrange(...)`。 - 運算子:`&&`(重疊)、`@>`/`<@`(包含,右邊可以是範圍或單一時間點)、`-|-`(相鄰)、`<<`/`>>`、`&<`/`&>`、`+`(聯集)、`*`(交集)、`-`(差集),以及 `=`、`<` 等排序比較。 - 函式:`lower`、`upper`、`isempty`、`lower_inc`、`upper_inc`、`lower_inf`、`upper_inf`、`range_merge`;聚合 `range_agg`(回傳 multirange)、`range_intersect_agg`;`unnest(multirange)`。 - 長度:`upper(r) - lower(r)` 會得到 `nanointerval`。 - 索引:GiST(`range_ops`,支援 `&&`、`@>`、`<@`、`-|-` 等)、B-tree(排序、`DISTINCT`)、hash(`GROUP BY`、hash join);`EXCLUDE USING gist` 排他約束。 - 文字與二進位 I/O:邊界保留 9 位小數;`COPY ... (FORMAT binary)` 可以往返。 - 不支援 `infinity` 邊界(子型別本身不支援),請用沒有邊界的寫法,例如 `'[2026-10-26 09:00+08,)'`。 ### 設計決定 - **subtype_diff 回傳「秒」(float8)。**`nanotimestamptz_subdiff(a, b)`/`nanotimestamp_subdiff(a, b)` 回傳 `a - b` 的秒數,和內建 `tstzrange_subdiff` 單位相同。GiST 建索引時用它估算「放進哪個節點比較好」(penalty、picksplit),`ANALYZE` 也用它建立範圍長度直方圖;這些都只看相對大小,所以單位本身不影響正確性,選「秒」是為了和內建型別一致。計算時先在 int128 中精確相減,再轉成 float8 一次,所以 1 奈秒的差距會得到 `1e-09`;如果把秒與奈秒分開轉換,`1 s - 0.999999999 s` 這類情況會因相消而失準(開發時實際遇到,已修正並加入回歸測試)。相差很大時(例如數十年),float8 無法表示到奈秒,但這只影響索引的估算,不影響 `&&`、`@>` 等判斷的結果,判斷一律用子型別的精確比較。 - **連續型範圍,沒有 canonical 函式,和 `tstzrange` 一樣。**雖然奈秒是最小單位,但我們不把 `[a, b]` 自動改寫成 `[a, b+1ns)`,理由是:與內建 `tstzrange`/`tsrange` 的行為一致、保留使用者寫下的邊界,也避免在型別最大值附近 `+1ns` 溢位。代價是:`[t, t+1ns]` 和 `[t, t+2ns)` 包含的時間點相同,但不視為相等;`[t, t+1ns]` 和 `[t+2ns, …)` 中間雖然沒有任何可表示的時間點,`-|-` 仍然回傳 false。**建議一律使用半開區間 `[開始, 結束)`**:前一段的結束剛好是下一段的開始,就會被判定為相鄰、不重疊,這也是預約系統最自然的寫法。 - **不提供 8 bytes 版的範圍型別。**`nanotimestamptz8` 可以隱含轉型成 `nanotimestamptz`,`nanotstzrange(ts8_a, ts8_b)` 直接可用。另外做一個 8 bytes 的範圍型別會多出一整組型別、multirange 與建構函式,換來的只是每個邊界少 8 bytes,目前不值得;有實際需求再評估。 - **奈秒邊界實測(回歸測試 `range`):**`[t, t+1ns)` 與 `[t+1ns, t+2ns)` 相鄰、不重疊;把前者改成 `[t, t+1ns]` 就會剛好共用 1 奈秒(交集是 `[t+1ns, t+1ns]`);只差 1 奈秒的兩個範圍不相等,排序也分得出先後;排他約束會擋下只重疊 1 奈秒的預約。 ## English pg_nanotime is a PostgreSQL 17/18 extension providing nanosecond timestamps, inspired by timestamp9 and building on that idea. Source: [bunko666-cloud/pg_nanotime](https://github.com/bunko666-cloud/pg_nanotime). - **Types**: - `nanotimestamptz`: 16 bytes, the full PostgreSQL timestamp range. - `nanotimestamptz8`: 8 bytes, int64 Unix ns, 1677–2262; binary-compatible with bigint, so TimescaleDB uses it as a native dimension. - `nanotimestamp`: without time zone. - `nanointerval`: fixed nanoseconds. - Out-of-range input, casts and arithmetic raise errors instead of wrapping around. - **Indexes and functions**: B-tree, hash and BRIN; `ts - ts`, `± nanointerval` and `± interval`; `time_bucket_ns`, `date_trunc`, `extract_ns`, `timezone()`. - **Clocks**: - `now_ns()`, `statement_ns()`, `clock_ns()` (and `*_ns8` variants). - `clock_ns_monotonic()` is strictly increasing within a session. - `clock_ns_global()` is strictly increasing and unique across sessions; it needs `shared_preload_libraries`. - The clock is selected with `nanotime.clock_source = realtime | tai`. - `nanotime_clock_status()` reports clock regressions and drift. - **Migration**: `nanotime_migrate_column()` converts timestamp9, bigint ns, timestamptz and timestamp columns in a single `ALTER TABLE`, rebuilding indexes. - **TimescaleDB**: `ns()`, `nanotime_now_int8()`, `nanotime_part*()`; 0.3.0 adds `nanotime_timescale_setup()` (hypertable + integer-now + `(col DESC)` index in one call) and `nanotime_drop_chunks()` (nanotimestamptz bounds). pg_nanotime does not depend on TimescaleDB; both call it through dynamic SQL. See docs/TIMESCALEDB.md. - **Range types (0.3.0)**: `nanotstzrange` / `nanotsrange` and their multiranges, built with `CREATE TYPE ... AS RANGE`. All built-in range operators, `range_agg`, GiST indexes and `EXCLUDE USING gist` work. They are continuous like `tstzrange` (no canonical function; prefer `[start, end)`), and subtype_diff returns seconds as float8, like `tstzrange_subdiff`. Install with `make && sudo make install` (PGXS), then `CREATE EXTENSION pg_nanotime;`. Run the tests with `make installcheck`. Detailed docs are in Traditional Chinese; test results, with real measured numbers, are in docs/TESTING.md. License: PostgreSQL.