Presto原始碼分析之資料型別

gamebus發表於2021-09-09

Presto作為一個計算引擎,除了支援一些常見的數字、字串型別的資料,還支援一些別的系統裡面比較少見的自定義的 IpAddress, Geometry 等等高階型別,今天來分析一下這些好玩的資料型別。

整數型別

tinyint, smallint, integer, bigint 是幾種整數型別,但是跟通常的資料庫不一樣的是,Presto裡面的資料都是 signed 型別,下面是這些資料型別的一個基本資訊:

型別 型別Size(Byte) 最大值 最小值
tinyint 1 -128 127
smallint 2 -32768 32767
integer 4 -2147483648 -2147483647
bigint 8 -9223372036854775808 9223372036854775807

Boolean

Boolean在底層是用 Byte 來表示的, 1代表true, 0代表false。

浮點數

在一般的程式語言裡面浮點數會有兩種型別: floatdouble , 在Presto裡面對應的是 realdouble, real 其實就是 float, 只是用了一個更專業化的名字。

real 在Presto裡面是用一個int來表示的:

    @Override
    public Object getObjectValue(ConnectorSession session, Block block, int position)
    {        if (block.isNull(position)) {            return null;
        }        return intBitsToFloat(block.getInt(position, 0)); // 看這裡: block.getInt()
    }

需要獲取實際的值的時候才會用 Float.intBitsToFloat 來進行轉換,為什麼可以用一個int來表示一個float? 因為它們在記憶體表示的時候都是用的4個位元組來表示的,佔用的儲存空間是一樣的。

那為什麼不直接用 float 自己來表示呢? 看下程式碼我們會發現Presto的 Block 類裡面只有針對整數的方法, 沒有浮點數:

圖片描述

Block裡面所有的Getters

浮點數的不精確性

這裡稍微展開一下,我們知道浮點數跟定點數不一樣的是,它們無法精確無損的表達所有的數。以float為例,根據 IEEE-754 它在記憶體裡面的表示方法是:

圖片描述

Float的表示

我們程式碼裡面寫的十進位制的數字在記憶體裡面實際是用如上的二進位制表示的, 它一共分為三段:

  • 第一位是符號位,用來表示這個數字的正負。

  • 第二到九位,用來表示指數(exponent)。

  • 剩下的23位,用來表示有效數字(Significant Figures)。

把上面的二進位制表示換算成十進位制的公式如下:

圖片描述

Float二進位制轉十進位制

由於各種原因,10進位制的整數和小數用二進位制的float都可能無法準確表示,首先來看看整數,整數在理論上都是可以無損的轉換成二進位制的,但是由於Float一共只有32位,其中只有23位用來表示有效數字(Significant Figures), 因此即使一個很小的數用Float都可能無法無損表示,比如: 20014999 , 它的完整二進位制表示應該是:

0 10010111 1001100010110011110010111 (33位)

但是由於float一共只有32位,最後幾位被截斷了,實際的二進位制表示是:

0 10010111 00110001011001111001100 (31位)

那自然就會有精度丟失了,這就解釋了雖然 2001499 不是一個很大的數,而且是一個整數,但是用float無法精確表示。

類似的,10進位制的小數在理論上就不一定 能用二進位制完全表示,比如 0.9 用二進位制表示是:

1100 1100 1100 1100 ... (1100一直重複)

由於有效位數是無窮大的(因為在無限迴圈),不管你精度是多少都無法無損的表示0.9這個數。

Double型別跟Float型別有類似的特點和類似的問題。它使用52位來表示有效數字  (float是23位) ,因此它的精度更高;它有11位(float是8位)來表示指數(exponent),因此它能表示的數字的範圍更大。

既然浮點數有這麼明顯的精度問題,為什麼我們還要用? 原因在於相對於定點數來說浮點數以相同的儲存空間可以表示更大範圍的數字, 比如同樣使用4個位元組來表示,int型別能表示的最大的數字是 (2 ^ 31 - 1) , 而Float能表示的最大的數字則是: (2 − 2 ^ −23) × (2 ^ 127) 這可大的太多了,在一些非金融領域使用float, double完全沒問題,但是一旦涉及到金融領域,必須要用定點數了。

定點數 Decimal

Decimal跟普通浮點數不一樣的是,它在宣告的時候有兩個關鍵引數: precisionscale:

decimal(3, 1)

這裡的 3 是precision, 而 1 是scale,所謂的 precision, 表示這個decimal的數
字裡面一共有多少個digits, 而scale表示的是小數點後面可以有多少個digits, 比如我
們上面例子裡面這個型別小數點前面最多2個數字,小數點後面最多1個數字, 也就是說最大值為: 99.9

decimal還有一些其它的宣告形式如下:

decimal // == decimal(10, 0)
decimal(20) // == decimal(20, 0)

Decimal型別在Presto裡面是用 BigInteger + (precision, scale) 資訊來一起表示的:

    // LongDecimalType.java
    @Override
    public Object getObjectValue(ConnectorSession session, Block block, int position)
    {        if (block.isNull(position)) {            return null;
        }
        Slice slice = block.getSlice(position, 0, getFixedSize());        return new SqlDecimal(decodeUnscaledValue(slice), getPrecision(), getScale());
    }    
    // SqlDecimal.java
    public final class SqlDecimal
    {        private final BigInteger unscaledValue;        private final int precision;        private final int scale;

Decimal在Presto裡面又分為兩種型別ShortDecimalTypeLongDecimalType, Short的版本最大的Precision是 18, 而Long的版本最大的Precision是 38 。分兩種型別的主要目的是為了效能,Short版本的效能更好,而且我們通常也確實使用Short版本的就夠了。這兩種版本是內部實現細節,使用者不需要感知這個。

Java裡面的Decimal -- BigDecimal

定點數由於完全準確的儲存了數值,沒有什麼十進位制與二進位制之間的轉換, 因此可以完全精準的儲存資料,我們來看看Java的Decimal實現: BigDecimal是怎麼儲存Decimal的資料的:

圖片描述

Java裡面的BigDecimal

我們可以看到,BigDecimal 為了最佳化效能和記憶體佔用分了兩種情況對資料進行儲存:

  • 不管是哪種情況,都透過 precisionscale 兩個欄位來儲存精度資訊

  • 如果資料不大(比Long.MAX_VALUE)小,那麼它會直接把數字儲存在intCompact裡面  (intCompact其實是一個long型別的欄位)。

  • 如果資料確實很大,超過了Long型別的範圍, 它會使用BigInteger型別的 intVal 來保  存scale過後的值。

    • 而BigInteger裡面則是透過一個int欄位的 signum 和 一個int陣列: mag 來表達。

其實我們上面的例子裡面舉的這個 bigDecimal 值並不是特別大,用 double 表示 8個位元組就夠了,而BigDecimal來表示的時候光是一個mag的int陣列就有三個int, 佔用了12個byte。因此Decimal型別其實是透過空間的消耗來換取的精度的準確。

字串型別

Presto裡面支援4種字串型別: varchar, char, varbinary, json

varchar 是一種可變長的字串型別, 你可以指定一個可選的最大長度, 比如 varchar 表示這個欄位的長度沒有上限(unbounded), 而 varchar(10) 則表示這個字串最大可以容納10個字元,但是也可以只容納5個字元,因此一個型別 varchar(5) 的值跟一個varchar(10) 的值是可能相等的。

char 是一種定長的字串型別,跟 char 類似長度也是可選的, 你如果不寫長度,那麼預設長度就是1: char == char(1) 。而如果你指定了長度,而最終你資料的長度又沒有那麼長,那麼會在尾部自動填充空格, 比如我們定義了 char(10) 型別的欄位,我們填充一個 hello 進去,那麼最終儲存的值其實是 hello_____ (因為顯示問題,這裡用下劃線代替空格), 因此兩個不同長度型別的 char 的值是絕對不可能相等的。

varbianry 表示的一種可變長的二進位制字串(binary string), 所謂的 bianry string也是一種string, 跟普通的string的區別在於普通的string是character string, 也就是說字串裡面的元素不一樣: 一個是 byte, 一個是 char。 Presto裡面的varbinary目前不接受最大長度的引數,也就是說所有的 varbinary 都是unbounded。

json 型別儲存的JSON型別的資料,可能是簡單型別: string, boolean, 數字, 也可能是複雜型別比如: JSONObject, JSONArray等等。

時間型別

時間型別主要有7種: date, time, time with time zone, timestamp,
timestamp with time zone, interval year to month, interval day to second

date 表示的是日期(不帶時分秒部分), Presto 裡面是用從 1970-01-01 到現在的天數來表示的, 從它的實現 SqlDate 就可以看出來了:

public final class SqlDate{    private final int days;    // TODO accept long
    public SqlDate(int days)
    {        this.days = days;
    }
    ...
}

time 表示的是時間(不帶日期部分), Presto內部儲存的是從UTC的
1970-01-01T00:00:00 到指定時間的毫秒數,由於時間跟時區是有關的,因此計算的時候一定會把當前session的時間傳入加入計算的。

timestamp 這是 datetime 的結合,既有日期,也有時間,而且也是從UTC的1970-01-01T00:00:00開始算的,這個 timestamp 欄位值的timezone取的是客戶端的TimeZone.

timestamp with time zone 顧名思義, 這個型別的資料的值裡面是自帶了時區的, 比如: TIMESTAMP '2001-08-22 03:04:05.321 America/Los_Angeles'

剩下的兩種資料型別是 interval 型別的,表示時間的間隔。這兩種型別貌似是從
Oracle 裡面借鑑過來的,其中 interval day to second, 表示的是天、時、分、秒級別的時間間隔, Presto內部儲存的是時間間隔用毫秒來表示的長度;而 interval year tomonth 表示的這是年、月級別的時間間隔,Presto內部儲存的月份的數量。

結構化的資料型別

Presto支援三種結構化的資料型別: ARRAY, MAP, ROW

ARRAY 很好理解,就是一個陣列,陣列裡面的元素的型別必須一致:

mysql> select ARRAY[1, 2, 3];
+----------------+| ARRAY[1, 2, 3] |
+----------------+| [1, 2, 3]      |
+----------------+1 row in set (0.11 sec)

MAP 表示是一個對映型別,跟JSON不一樣的是,所有的key的型別必須一致,所有value的型別也必須一致。在字面量裡面,Presto是透過讓使用者指定兩個有序ARRAY: 一個key的ARRAY,一個value的ARRAY來表達的:

mysql> select MAP(ARRAY['foo', 'bar', 'hello'], ARRAY[1, 2, 3]);
+---------------------------------------------------+| MAP(ARRAY['foo', 'bar', 'hello'], ARRAY[1, 2, 3]) |
+---------------------------------------------------+| {bar=2, foo=1, hello=3}                           |
+---------------------------------------------------+1 row in set (0.11 sec)

在記憶體裡面的表示,MAP 的內容這是被儲存成一個一個的key-value對:

        // MapType.java
        for (int i = 0; i < singleMapBlock.getPositionCount(); i += 2) {
            map.put(
                    keyType.getObjectValue(session, singleMapBlock, i),
                    valueType.getObjectValue(session, singleMapBlock, i + 1)
            );
        }

ROW 表示的是一行記錄,這行記錄的資料可以是各種不同的型別,比如:

mysql> select ROW(1, 2.0);
+-------------+| ROW(1, 2.0) |
+-------------+| [1, 2.0]    |
+-------------+1 row in set (0.32 sec)

IpAddress

IpAddress是一個蠻有意思的型別,它可以表示IPV4和IPV6的IP地址, 你可以透過下面的語句來試試這種型別:

CREATE TABLE foo (
       a VARCHAR, 
       b BIGINT,
       c IPADDRESS
)

IPADDRESS之間可以進行比較, 支援一些操作包括 =, >, '<' 等等, 同時 IPADDRESS和 VARCHAR兩種型別之間可以進行CAST。比如:

CAST (ipaddress AS VARCHAR)

因為IpAdress內部儲存都是以IPV6的形式來存的(IPV4也會被轉成IPV6), 而IPV6是128位的,因此從儲存空間佔用上來看,IpAddress類似於BINARY(16)

Geometry

Geometry型別是表示幾何學上的一些資訊,它表達的一一組相關的型別以及一些輔助函式的幾何,比如點(Point)、線(LineStrin)、多邊形(Polygon)等,用Oracle文件上的一張圖來看特別直觀:

圖片描述

Geometric Types

Geometry是一類很有意思的資料,Presto裡面提供了大量相關的函式,比如 ST_Crosses 來判斷兩個幾何圖形是否有交集, 再比如 ST_Equals 來表示兩個圖形表示的是否是同一個圖形等等。

BingTile

BingTile 也是一個地理位置相關的型別, 它表示的是微軟Bing地圖服務上地圖的一個指定區域。首先Bing把整個地圖對映到一個平面上面:

圖片描述

二維世界地圖

這樣地球上的任何一個點都可以用一個二維的座標 (X,Y) 來定位了。我們平時看地圖的時候經常對地圖進行縮放,不同程度的縮放對應的地圖的詳細程度是不一樣的,這樣地圖就會有一個縮放因子的引數(zoomLevel)。相同的座標在不通的縮放因子上對應的地理位置是不一樣的。

但是通常我們獲取一個座標沒太大意義,更多的時候我們是要獲取指定的一塊區域,為了高效的獲取一個指定區域的地圖,Bing把整個地球的地圖分成了很多小份,每一份叫做一個 Tile, 每個 Tile 的大小是 256 x 256(pixel)。

圖片描述

Bing Tile

這樣每個Tile也有了座標,我們指定特定的的縮放程度以及對應的座標,我們就可以獲得指定區域的地圖了, 我們看看 Presto 的程式碼也可以印證這一點:

public final class BingTile{    public static final int MAX_ZOOM_LEVEL = 23;    private final int x;    private final int y;    private final int zoomLevel;
    ...
}

HyperLogLog 和 P4HyperLogLog

HyperLogLog 是一種計算 count-distinct 的近似演算法,我們知道要對資料集資料進行 count-distinct 的計算,需要的記憶體量跟要計算的資料量是成正比的,HyperLogLog演算法可以對 10 ^ 9 以上的資料量進行高效的 count-distinct 計算,所需要的記憶體僅為 1.5KB, 而準確性為 98% 左右。P4HyerLogLog 表示的也是同一個東西,只是使用的演算法稍有差異。

總結

今天我們分析了Presto裡面的所有標準資料型別,除了常見的簡單型別,還有一些高階的自定義型別,Presto裡面新增自定義型別很簡單,後面有機會專門分析一下。



作者:xumingmingv
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4687/viewspace-2817365/,如需轉載,請註明出處,否則將追究法律責任。

相關文章