在關於編寫Postgres擴展的第三部分中,咱們使用LLDB調試器修復了一個嚴重的錯誤,並使用類型轉換完成了base36
類型。如今是時候恢復咱們實際上已經取得的成就——並作更多的測試。html
你能夠在github branch part_iii上查看當前代碼庫。git
只是簡單地在Postgres-console中嘗試一些東西就論斷一切均可以正常工做是一個壞主意,特別是在開發擴展時引入了一些嚴重的bug以後。所以,咱們瞭解到擁有一個徹底覆蓋的測試套件是多麼重要,它不只能夠測試「快樂路徑」,還能夠測試邊緣和錯誤狀況。github
在第一篇文章中,咱們已經在測試方面作得很好,咱們使用了內置的擴展迴歸測試。因此讓咱們在一些測試腳本中寫下咱們的發現。sql
文件名:sql/base36_test.sqlshell
CREATE EXTENSION base36; SELECT '120'::base36; SELECT '3c'::base36; CREATE TABLE base36_test(val base36); INSERT INTO base36_test VALUES ('123'), ('3c'), ('5A'), ('zZz'); SELECT * FROM base36_test; SELECT '120'::base36 > '3c'::base36; SELECT * FROM base36_test ORDER BY val; EXPLAIN (COSTS OFF) SELECT * FROM base36_test where NOT val < 'c1'; SELECT 'abcdefghi'::base36;
注意,我在EXPLAIN
命令中添加了(COSTS OFF
),以確保測試不會在具備不一樣成本參數的不一樣機器上失敗。數據庫
若是咱們如今運行:安全
make clean && make && make install && make installcheck
咱們在results/base36_test.out
中獲取輸出,並將其複製到sql/expected /
。 但等等 - 讓咱們先仔細閱讀,以確保這一切都符合預期。函數
SELECT 'abcdefghi'::base36; base36 -------- r0bprq (1 row)
顯然不符合預期。當咱們在base36_in
中放太長的字符串時,它彷佛也有一個嚴重的錯誤。讓咱們看看strtol
的文檔:post
man strtol strtoimax, strtol, strtoll, strtoq -- convert a string value to a long, long long, intmax_t or quad_t integer
因此在第13行中咱們將一個long int
轉換爲一個int
型發生了溢出。性能
result = strtol(str, NULL, 36);
讓咱們經過再次重用Postgres內部功能來地進行正確的轉換:那麼Postgres如何將bigint
轉換爲int
呢?
test=# \dC bigint List of casts Source type | Target type | Function | Implicit? --------------+-----------------------+--------------------+--------------- bigint | bit | bit | no bigint | double precision | float8 | yes bigint | integer | int4 | in assignment
這裏使用的sql函數int4
是如何定義的?
test=# \df+ int4 List of functions Name | Result data type | Argument data types | Source code ------+------------------+---------------------+--------------------- int4 | integer | "char" | chartoi4 int4 | integer | bigint | int84 int4 | integer | bit | bittoint4 int4 | integer | boolean | bool_int4 int4 | integer | double precision | dtoi4 int4 | integer | numeric | numeric_int4 int4 | integer | real | ftoi4 int4 | integer | smallint | i2toi4 (8 rows)
因此int84
就是咱們要找的。你能夠在utils/int8.h
中找到它的定義,咱們須要將它include到源代碼中才能使用它。你已經在第一篇文章中瞭解到,爲了在SQL中使用C函數,你必須使用「版本1」調用約定來定義它們。所以,這些函數具備int84的特定簽名:
extern Datum int84(PG_FUNCTION_ARGS);
因此咱們不能直接從代碼中調用這個函數,咱們必須使用來自fmgr.h
的DirectFunctionCall
宏:
DirectFunctionCall1(func, arg1) DirectFunctionCall2(func, arg1, arg2) DirectFunctionCall3(func, arg1, arg2, arg3) DirectFunctionCall4(func, arg1, arg2, arg3, arg4) DirectFunctionCall5(func, arg1, arg2, arg3, arg4, arg5) DirectFunctionCall6(func, arg1, arg2, arg3, arg4, arg5, arg6) DirectFunctionCall7(func, arg1, arg2, arg3, arg4, arg5, arg6, arg7) DirectFunctionCall8(func, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) DirectFunctionCall9(func, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
有了這些宏,咱們能夠根據參數的數量在咱們的C代碼中直接調用任何函數。可是使用這些宏時要當心:這些宏不是類型安全的,由於傳遞和返回的參數只是Datums
,而Datums
能夠是任何類型的數據。使用這個你不會從編譯器獲得錯誤。若是你傳遞了錯誤的數據類型,你只會在運行時獲得奇怪的結果——這是擁有一個徹底覆蓋的測試套件的又一個緣由。
因爲宏已經返回了一個Datum
類型的數據,咱們最終獲得:
文件名:base36.c
PG_FUNCTION_INFO_V1(base36_in); Datum base36_in(PG_FUNCTION_ARGS) { long result; char *str = PG_GETARG_CSTRING(0); result = strtol(str, NULL, 36); PG_RETURN_DATUM(DirectFunctionCall1(int84,(int64)result)); }
最後:
# SELECT 'abcdefghi'::base36; ERROR: integer out of range LINE 1: SELECT 'abcdefghi'::base36;
爲了更好地瞭解不一樣的測試,讓咱們將它們分紅不一樣的文件並將它們存儲在test/sql
目錄下。爲了實現這一點,咱們還須要調整Makefile。
文件名:Makefile
EXTENSION = base36 # the extensions name DATA = base36--0.0.1.sql # script files to install TESTS = $(wildcard test/sql/*.sql) # use test/sql/*.sql as test files # find the sql and expected directories under test # load base36 extension into test db # load plpgsql into test db REGRESS_OPTS = --inputdir=test \ --load-extension=base36 \ --load-language=plpgsql REGRESS = $(patsubst test/sql/%.sql,%,$(TESTS)) MODULES = base36 # our c module file to build # postgres build stuff PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS)
TESTS
定義了咱們在test/sql /* .sql
下能夠找到的不一樣測試文件。此外,咱們還添加了REGRESS選項,將測試輸入目錄更改成test
(—inputdir=test
),迴歸運行程序指望sql
目錄包含測試腳本,expected
目錄包含預期輸出。咱們還定義了擴展base36
應該事先在測試數據庫中建立(--load-extension = base36
),避免在每一個測試腳本的頂部運行CREATE EXTENSION
命令。咱們還定義了將plpgsql語言加載到測試數據庫中,這實際上不是咱們的測試套件所須要的。可是它不會形成傷害,而且爲咱們將來的項目提供了一個更通用的Makefile。
如今讓咱們添加測試文件:
文件名:test/sql/base36_io.sql
-- simple input SELECT '120'::base36; SELECT '3c'::base36; -- case insensitivity SELECT '3C'::base36; SELECT 'FoO'::base36; -- invalid characters SELECT 'foo bar'::base36; SELECT 'abc$%2'::base36; -- negative values SELECT '-10'::base36; -- too big values SELECT 'abcdefghi'::base36; -- storage BEGIN; CREATE TABLE base36_test(val base36); INSERT INTO base36_test VALUES ('123'), ('3c'), ('5A'), ('zZz'); SELECT * FROM base36_test; UPDATE base36_test SET val = '567a' where val = '123'; SELECT * FROM base36_test; ROLLBACK;
注意,我將狀態更改命令封裝在一個事務中,該事務將在最後回滾。這是爲了確保每一個腳本都以一個乾淨的狀態開始。若是咱們如今看看咱們在results/base36_io
中獲得了什麼,咱們會發現咱們在惡意輸入上又有了一些有趣的行爲。
-- invalid characters SELECT 'foo bar'::base36; base36 -------- foo (1 row) SELECT 'abc$%2'::base36; base36 -------- abc (1 row)
strtol
函數轉換爲給定的基數,在字符串的末尾或在給定基數中不產生有效數字的第一個字符處中止。咱們絕對不想要這個驚喜,因此讓咱們閱讀man page(man strtol
)並修復它。
If endptr is not NULL, strtol() stores the address of the first invalid character in *endptr. If there were no digits at all, however, strtol() stores the original value of str in *endptr. (Thus, if *str is not `\0' but **endptr is `\0' on return, the entire string was valid.)
文件名:
PG_FUNCTION_INFO_V1(base36_in); Datum base36_in(PG_FUNCTION_ARGS) { long result; char *bad; char *str = PG_GETARG_CSTRING(0); result = strtol(str, &bad, 36); if (bad[0] != '\0' || strlen(str)==0) ereport(ERROR, ( errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid input syntax for base36: \"%s\"", str) ) ); PG_RETURN_DATUM(DirectFunctionCall1(int84,(int64)result)); }
運行make clean && make && make install && make installcheck
後,results / base36_io.out
看起來不錯。 讓咱們將其複製到預期的文件夾中:
mkdir test/expected cp results/base36_io.out test/expected
並從新運行咱們的測試套件
make clean && make && make install && make installcheck
文件名:test/sql/operators.sql
-- comparison SELECT '120'::base36 > '3c'::base36; SELECT '120'::base36 >= '3c'::base36; SELECT '120'::base36 < '3c'::base36; SELECT '120'::base36 <= '3c'::base36; SELECT '120'::base36 <> '3c'::base36; SELECT '120'::base36 = '3c'::base36; -- comparison equals SELECT '120'::base36 > '120'::base36; SELECT '120'::base36 >= '120'::base36; SELECT '120'::base36 < '120'::base36; SELECT '120'::base36 <= '120'::base36; SELECT '120'::base36 <> '120'::base36; SELECT '120'::base36 = '120'::base36; -- comparison negation SELECT NOT '120'::base36 > '120'::base36; SELECT NOT '120'::base36 >= '120'::base36; SELECT NOT '120'::base36 < '120'::base36; SELECT NOT '120'::base36 <= '120'::base36; SELECT NOT '120'::base36 <> '120'::base36; SELECT NOT '120'::base36 = '120'::base36; --commutator and negator BEGIN; CREATE TABLE base36_test AS SELECT i::base36 as val FROM generate_series(1,10000) i; CREATE INDEX ON base36_test(val); ANALYZE; SET enable_seqscan TO off; EXPLAIN (COSTS OFF) SELECT * FROM base36_test where NOT val < 'c1'; EXPLAIN (COSTS OFF) SELECT * FROM base36_test where NOT 'c1' > val; EXPLAIN (COSTS OFF) SELECT * FROM base36_test where 'c1' > val; -- hash aggregate SET enable_seqscan TO on; EXPLAIN (COSTS OFF) SELECT val, COUNT(*) FROM base36_test GROUP BY 1; ROLLBACK;
這裏咱們使用了一些運行時查詢配置來強制使用索引和哈希聚合。
SET enable_seqscan TO off; EXPLAIN (COSTS OFF) SELECT * FROM base36_test where NOT val < 'c1'; QUERY PLAN ---------------------------------------------------------- Index Only Scan using base36_test_val_idx on base36_test Index Cond: (val >= 'c1'::base36) (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM base36_test where NOT 'c1' > val; QUERY PLAN ---------------------------------------------------------- Index Only Scan using base36_test_val_idx on base36_test Index Cond: (val >= 'c1'::base36) (2 rows) EXPLAIN (COSTS OFF) SELECT * FROM base36_test where 'c1' > val; QUERY PLAN ---------------------------------------------------------- Index Only Scan using base36_test_val_idx on base36_test Index Cond: (val < 'c1'::base36) (2 rows) -- hash aggregate SET enable_seqscan TO on; EXPLAIN (COSTS OFF) SELECT val, COUNT(*) FROM base36_test GROUP BY 1; QUERY PLAN ------------------------------- HashAggregate Group Key: val -> Seq Scan on base36_test (3 rows)
所以,咱們能夠確保COMMUTATOR
和NEGATOR
的設置是正確的。
由於咱們沒有編寫太多本身的代碼,而是使用了Postgres的內部功能,咱們看到results / operators.out
看起來不錯。 咱們一樣複製它。
cp results/operators.out test/expected make clean && make && make install && make installcheck
獲得
============== running regression test queries ============== test base36_io ... ok test operators ... ok ===================== All 2 tests passed. =====================
到目前爲止,咱們實現了輸入和輸出函數,重用了Postgres比較函數和操做符,並對全部內容進行了測試。咱們作完了嗎?不!咱們還能夠添加一個測試:
文件名:test/sql/operators.sql
-- storage BEGIN; CREATE TABLE base36_test(val base36); INSERT INTO base36_test VALUES ('123'), ('3c'), ('5A'), ('zZz'); SELECT * FROM base36_test; UPDATE base36_test SET val = '567a' where val = '123'; SELECT * FROM base36_test; UPDATE base36_test SET val = '-aa' where val = '3c'; SELECT * FROM base36_test; ROLLBACK;
在這裏,咱們嘗試更新到一個負值,應該會失敗:
UPDATE base36_test SET val = '-aa' where val = '3c'; SELECT * FROM base36_test; ERROR: negative values are not allowed DETAIL: value -370 is negative HINT: make it positive
但它沒有...嗯,它確實,但不是在更新步驟 - 只有在檢索值時。雖然咱們不容許輸出函數爲負值,但它仍然容許輸入值爲負值。當咱們執行如下命令時
SELECT '-aa'::base36; ERROR: negative values are not allowed DETAIL: value -370 is negative HINT: make it positive
同時調用輸入和輸出函數,致使錯誤.可是對於UPDATE命令,只調用輸入,致使磁盤上出現一個負值,以後將永遠沒法檢索該值。讓咱們快速解決這個問題
文件名:base36.c
PG_FUNCTION_INFO_V1(base36_in); Datum base36_in(PG_FUNCTION_ARGS) { int64 result; char *bad; char *str = PG_GETARG_CSTRING(0); result = strtol(str, &bad, 36); if (bad[0] != '\0' || strlen(str)==0) ereport(ERROR, ( errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid input syntax for base36: \"%s\"", str) ) ); if (result < 0) ereport(ERROR, ( errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("negative values are not allowed"), errdetail("value %ld is negative", result), errhint("make it positive") ) ); PG_RETURN_DATUM(DirectFunctionCall1(int84,result)); }
雖然擴展Postgres頗有趣,可是不要忘記咱們爲何要構建全部這些。讓咱們將base36
方法與使用varchar
類型的Postgres-native方法進行比較。咱們將比較兩個方面:每種類型的存儲需求和相應的查詢性能。
咱們最初的動機是節省空間,只存儲4個字節的整數而不是6個字符,根據文檔,這將浪費7個字節。
讓咱們比較一下。
test=# CREATE TABLE base36_check (val base36); CREATE TABLE test=# CREATE TABLE varchar_check (val varchar(6)); CREATE TABLE test=# INSERT INTO base36_check SELECT i::base36 from generate_series(1,1e6::int) i; INSERT 0 1000000 test=# INSERT INTO varchar_check SELECT i::base36::text from generate_series(1,1e6::int) i; INSERT 0 1000000 test=# SELECT pg_table_size('base36_check') as "base36 size", pg_table_size('varchar_check') as "varchar_check size"; base36 size | varchar_check size -------------+----------------------- 36249600 | 36249600 (1 row)
哎呀......咱們沒有保存一個字節! 對於咱們在數據類型中所作的全部努力,這是很是不幸的。這是怎麼發生的呢?咱們必須知道Postgres其實是如何存儲數據的。咱們的小示例將以如下內容結束:
因此咱們確實應該每行節省3個字節,但最終表大小竟然相同。咱們還須要考慮,Postgres將數據存儲在一般包含8kB(8192字節)數據的頁面中,而且單個行不能跨兩個頁面。每行最終還會有一個最大數據對齊設置的倍數,即現代64位系統上的8個字節。
因此最後,在這兩種狀況下,咱們須要每行32字節+4字節元組指針。
(8192 per page - 24 page header) ----------------------------------------------------- = 226 rows per page (32 byte data and alignment + 4 byte tuple pointer)
真實世界的例子中,狀況(固然)會徹底改變:
test=# DROP TABLE base36_check; DROP TABLE test=# DROP TABLE varchar_check; DROP TABLE test=# CREATE TABLE base36_check (val base36, num integer); CREATE TABLE test=# CREATE TABLE varchar_check (val varchar(6), num integer); CREATE TABLE test=# INSERT INTO base36_check SELECT i::base36, i from generate_series(1,1e6::int) i; INSERT 0 1000000 test=# INSERT INTO varchar_check SELECT i::base36::text,i from generate_series(1,1e6::int) i; INSERT 0 1000000 test=# SELECT pg_size_pretty(pg_table_size('base36_check')) as "base36 size", pg_size_pretty(pg_table_size('varchar_check')) as "varchar_check size"; base36 size | varchar_check size -------------+-------------------- 35 MB | 42 MB
當咱們向數據庫中添加數據時,因爲base36檢查表上的對齊浪費了4個字節,因此它沒有增加,而varchar檢查表每一行增長了4個字節的數據加上4個字節的對齊。
如今咱們節省了20%的空間。
咱們作一些計時
test=# \timing Timing is on. test=# SELECT * FROM varchar_check ORDER BY VAL LIMIT 10; val | num ------+------- 1 | 1 10 | 36 100 | 1296 1000 | 46656 1001 | 46657 1002 | 46658 1003 | 46659 1004 | 46660 1005 | 46661 1006 | 46662 (10 rows) Time: 601,551 ms test=# SELECT * FROM base36_check ORDER BY VAL LIMIT 10; val | num -----+----- 1 | 1 2 | 2 3 | 3 4 | 4 5 | 5 6 | 6 7 | 7 8 | 8 9 | 9 a | 10 (10 rows) Time: 73,575 ms
除了base36的排序更天然以外,它的速度也快了8倍。若是你記住排序是數據庫的關鍵操做,那麼這個事實爲咱們提供了真正的優化。 例如,在建立索引時:
test=# CREATE INDEX ON varchar_check(val); CREATE INDEX Time: 13585,451 ms test=# CREATE INDEX ON base36_check(val); CREATE INDEX Time: 294,433 ms
它對於鏈接操做或按語句分組也頗有用。
既然咱們已經修復了全部的bug並添加了測試來確保它們不會再出現,那麼咱們的擴展就差很少完成了。在本系列的下一篇文章中,咱們將使用bigbase36類型完成擴展,並看看如何更好地構造代碼。