Presto作為一個計算引擎蹄皱,除了支持一些常見的數(shù)字捧灰、字符串類型的數(shù)據(jù),還支持一些別的系統(tǒng)里面比較少見的自定義的 IpAddress, Geometry 等等高級類型屁使,今天來分析一下這些好玩的數(shù)據(jù)類型减细。
整數(shù)類型
tinyint
, smallint
, integer
, bigint
是幾種整數(shù)類型,但是跟通常的數(shù)據(jù)庫不一樣的是赢笨,Presto里面的數(shù)據(jù)都是 signed 類型未蝌,下面是這些數(shù)據(jù)類型的一個基本信息:
類型 | 類型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。
浮點數(shù)
在一般的編程語言里面浮點數(shù)會有兩種類型: float
和 double
, 在Presto里面對應的是 real
和 double
, real
其實就是 float
, 只是用了一個更專業(yè)化的名字萧吠。
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個字節(jié)來表示的,占用的存儲空間是一樣的纸型。
那為什么不直接用 float
自己來表示呢? 看下代碼我們會發(fā)現(xiàn)Presto的 Block
類里面只有針對整數(shù)的方法, 沒有浮點數(shù):
浮點數(shù)的不精確性
這里稍微展開一下拇砰,我們知道浮點數(shù)跟定點數(shù)不一樣的是,它們無法精確無損的表達所有的數(shù)狰腌。以float為例除破,根據(jù) IEEE-754 它在內存里面的表示方法是:
我們代碼里面寫的十進制的數(shù)字在內存里面實際是用如上的二進制表示的, 它一共分為三段:
- 第一位是符號位,用來表示這個數(shù)字的正負琼腔。
- 第二到九位瑰枫,用來表示指數(shù)(exponent)。
- 剩下的23位丹莲,用來表示有效數(shù)字(Significant Figures)光坝。
把上面的二進制表示換算成十進制的公式如下:
由于各種原因,10進制的整數(shù)和小數(shù)用二進制的float都可能無法準確表示甥材,首先來看看整數(shù)盯另,整數(shù)在理論上都是可以無損的轉換成二進制的,但是由于Float一共只有32位洲赵,其中只有23位用來表示有效數(shù)字(Significant Figures), 因此即使一個很小的數(shù)用Float都可能無法無損表示鸳惯,比如: 20014999
, 它的完整二進制表示應該是:
0 10010111 1001100010110011110010111 (33位)
但是由于float一共只有32位,最后幾位被截斷了板鬓,實際的二進制表示是:
0 10010111 00110001011001111001100 (31位)
那自然就會有精度丟失了悲敷,這就解釋了雖然 2001499
不是一個很大的數(shù),而且是一個整數(shù)俭令,但是用float無法精確表示后德。
類似的,10進制的小數(shù)在理論上就不一定 能用二進制完全表示抄腔,比如 0.9
用二進制表示是:
1100 1100 1100 1100 ... (1100一直重復)
由于有效位數(shù)是無窮大的(因為在無限循環(huán))瓢湃,不管你精度是多少都無法無損的表示0.9這個數(shù)。
Double類型跟Float類型有類似的特點和類似的問題赫蛇。它使用52位來表示有效數(shù)字 (float是23位) 绵患,因此它的精度更高;它有11位(float是8位)來表示指數(shù)(exponent)悟耘,因此它能表示的數(shù)字的范圍更大落蝙。
既然浮點數(shù)有這么明顯的精度問題,為什么我們還要用? 原因在于相對于定點數(shù)來說浮點數(shù)以相同的存儲空間可以表示更大范圍的數(shù)字, 比如同樣使用4個字節(jié)來表示,int類型能表示的最大的數(shù)字是 (2 ^ 31 - 1)
, 而Float能表示的最大的數(shù)字則是: (2 ? 2 ^ ?23) × (2 ^ 127)
這可大的太多了筏勒,在一些非金融領域使用float, double完全沒問題移迫,但是一旦涉及到金融領域,必須要用定點數(shù)了管行。
定點數(shù) Decimal
Decimal跟普通浮點數(shù)不一樣的是厨埋,它在聲明的時候有兩個關鍵參數(shù): precision
和 scale
:
decimal(3, 1)
這里的 3
是precision, 而 1
是scale,所謂的 precision, 表示這個decimal的數(shù)
字里面一共有多少個digits, 而scale表示的是小數(shù)點后面可以有多少個digits, 比如我
們上面例子里面這個類型小數(shù)點前面最多2個數(shù)字捐顷,小數(shù)點后面最多1個數(shù)字, 也就是說最大值為: 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里面又分為兩種類型ShortDecimalType
和 LongDecimalType
, Short的版本最大的Precision是 18
, 而Long的版本最大的Precision是 38
荡陷。分兩種類型的主要目的是為了性能,Short版本的性能更好迅涮,而且我們通常也確實使用Short版本的就夠了废赞。這兩種版本是內部實現(xiàn)細節(jié),用戶不需要感知這個逗柴。
Java里面的Decimal -- BigDecimal
定點數(shù)由于完全準確的存儲了數(shù)值蛹头,沒有什么十進制與二進制之間的轉換, 因此可以完全精準的存儲數(shù)據(jù),我們來看看Java的Decimal實現(xiàn): BigDecimal是怎么保存Decimal的數(shù)據(jù)的:
我們可以看到戏溺,BigDecimal 為了優(yōu)化性能和內存占用分了兩種情況對數(shù)據(jù)進行存儲:
- 不管是哪種情況渣蜗,都通過
precision
和scale
兩個字段來保存精度信息 - 如果數(shù)據(jù)不大(比Long.MAX_VALUE)小,那么它會直接把數(shù)字保存在intCompact里面 (intCompact其實是一個long類型的字段)旷祸。
- 如果數(shù)據(jù)確實很大耕拷,超過了Long類型的范圍, 它會使用BigInteger類型的
intVal
來保 存scale過后的值。- 而BigInteger里面則是通過一個int字段的
signum
和 一個int數(shù)組:mag
來表達托享。
- 而BigInteger里面則是通過一個int字段的
其實我們上面的例子里面舉的這個 bigDecimal
值并不是特別大骚烧,用 double
表示 8個字節(jié)就夠了,而BigDecimal來表示的時候光是一個mag的int數(shù)組就有三個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)
。而如果你指定了長度迄沫,而最終你數(shù)據(jù)的長度又沒有那么長稻扬,那么會在尾部自動填充空格, 比如我們定義了 char(10)
類型的字段,我們填充一個 hello
進去羊瘩,那么最終存儲的值其實是 hello_____
(因為顯示問題泰佳,這里用下劃線代替空格), 因此兩個不同長度類型的 char
的值是絕對不可能相等的盼砍。
varbianry
表示的一種可變長的二進制字符串(binary string), 所謂的 bianry string也是一種string, 跟普通的string的區(qū)別在于普通的string是character string, 也就是說字符串里面的元素不一樣: 一個是 byte
, 一個是 char
惋鹅。 Presto里面的varbinary
目前不接受最大長度的參數(shù)啤咽,也就是說所有的 varbinary
都是unbounded。
json
類型保存的JSON類型的數(shù)據(jù),可能是簡單類型: string
, boolean
, 數(shù)字, 也可能是復雜類型比如: 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
到現(xiàn)在的天數(shù)來表示的, 從它的實現(xiàn) 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
到指定時間的毫秒數(shù),由于時間跟時區(qū)是有關的侧戴,因此計算的時候一定會把當前session的時間傳入加入計算的宁昭。
timestamp
這是 date
和 time
的結合,既有日期酗宋,也有時間积仗,而且也是從UTC的1970-01-01T00:00:00
開始算的,這個 timestamp
字段值的timezone取的是客戶端的TimeZone.
timestamp with time zone
顧名思義, 這個類型的數(shù)據(jù)的值里面是自帶了時區(qū)的, 比如: TIMESTAMP '2001-08-22 03:04:05.321 America/Los_Angeles'
蜕猫。
剩下的兩種數(shù)據(jù)類型是 interval
類型的寂曹,表示時間的間隔。這兩種類型貌似是從
Oracle 里面借鑒過來的回右,其中 interval day to second
, 表示的是天隆圆、時、分翔烁、秒級別的時間間隔, Presto內部保存的是時間間隔用毫秒來表示的長度渺氧;而 interval year tomonth
表示的這是年、月級別的時間間隔蹬屹,Presto內部保存的月份的數(shù)量侣背。
結構化的數(shù)據(jù)類型
Presto支持三種結構化的數(shù)據(jù)類型: ARRAY
, MAP
, ROW
。
ARRAY
很好理解慨默,就是一個數(shù)組贩耐,數(shù)組里面的元素的類型必須一致:
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
表示的是一行記錄台谢,這行記錄的數(shù)據(jù)可以是各種不同的類型寻狂,比如:
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類型是表示幾何學上的一些信息纠亚,它表達的一一組相關的類型以及一些輔助函數(shù)的幾何塘慕,比如點(Point)、線(LineStrin)蒂胞、多邊形(Polygon)等图呢,用Oracle文檔上的一張圖來看特別直觀:
Geometry是一類很有意思的數(shù)據(jù),Presto里面提供了大量相關的函數(shù)骗随,比如 ST_Crosses
來判斷兩個幾何圖形是否有交集, 再比如 ST_Equals
來表示兩個圖形表示的是否是同一個圖形等等蛤织。
BingTile
BingTile 也是一個地理位置相關的類型, 它表示的是微軟Bing地圖服務上地圖的一個指定區(qū)域。首先Bing把整個地圖映射到一個平面上面:
這樣地球上的任何一個點都可以用一個二維的坐標 (X,Y)
來定位了鸿染。我們平時看地圖的時候經常對地圖進行縮放指蚜,不同程度的縮放對應的地圖的詳細程度是不一樣的,這樣地圖就會有一個縮放因子的參數(shù)(zoomLevel)涨椒。相同的坐標在不通的縮放因子上對應的地理位置是不一樣的摊鸡。
但是通常我們獲取一個坐標沒太大意義,更多的時候我們是要獲取指定的一塊區(qū)域蚕冬,為了高效的獲取一個指定區(qū)域的地圖免猾,Bing把整個地球的地圖分成了很多小份,每一份叫做一個 Tile
, 每個 Tile
的大小是 256 x 256
(pixel)播瞳。
這樣每個Tile也有了坐標掸刊,我們指定特定的的縮放程度以及對應的坐標,我們就可以獲得指定區(qū)域的地圖了, 我們看看 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
的近似算法赢乓,我們知道要對數(shù)據(jù)集數(shù)據(jù)進行 count-distinct
的計算忧侧,需要的內存量跟要計算的數(shù)據(jù)量是成正比的,HyperLogLog算法可以對 10 ^ 9 以上的數(shù)據(jù)量進行高效的 count-distinct
計算牌芋,所需要的內存僅為 1.5KB, 而準確性為 98%
左右蚓炬。P4HyerLogLog
表示的也是同一個東西,只是使用的算法稍有差異躺屁。
總結
今天我們分析了Presto里面的所有標準數(shù)據(jù)類型肯夏,除了常見的簡單類型,還有一些高級的自定義類型犀暑,Presto里面添加自定義類型很簡單驯击,后面有機會專門分析一下。