問(wèn)題提出
時(shí)常會(huì)思考一個(gè)問(wèn)題督函,SQL作為一種與數(shù)據(jù)交互的標(biāo)準(zhǔn)化語(yǔ)言隧土,可以說(shuō)是數(shù)據(jù)分析最強(qiáng)大的工具行楞。不管是關(guān)注事務(wù)的OLTP型數(shù)據(jù)庫(kù)或者是關(guān)注分析的OLAP型數(shù)據(jù)庫(kù),提供基本的SQL支持都是必須的海雪。
如果抽象一下锦爵,萬(wàn)事萬(wàn)物皆為數(shù)據(jù)的載體,一個(gè)excel喳魏,一個(gè)txt文本棉浸,甚至一個(gè)二維數(shù)組怀薛。如果要在內(nèi)存中對(duì)上述的結(jié)構(gòu)進(jìn)行操作刺彩,往往需要很復(fù)雜的讀取和遍歷操作,當(dāng)涉及到多列過(guò)濾,排序和分組時(shí)创倔,工作量呈幾何倍數(shù)增加嗡害。因此,我在實(shí)際工作中遇到的問(wèn)題就是畦攘,能否通過(guò)標(biāo)準(zhǔn)化的SQL來(lái)查詢內(nèi)存數(shù)據(jù)呢霸妹?
技術(shù)選型
首先能想到的方法有兩種:
1.借用中間數(shù)據(jù)庫(kù):將數(shù)據(jù)導(dǎo)入到某種數(shù)據(jù)庫(kù)中,例如MySQL或者Hive知押,這樣做的好處在于叹螟,可以直接借助數(shù)據(jù)庫(kù)引擎的SQL查詢能力,并且可以存儲(chǔ)數(shù)據(jù)台盯。缺點(diǎn)在于罢绽,如果需求并沒(méi)有存儲(chǔ)數(shù)據(jù)的要求,且要求查詢速率時(shí)静盅,該方法顯然不合適良价。
2.使用內(nèi)存數(shù)據(jù)庫(kù):想法與第一種類似,有沒(méi)有某種內(nèi)存數(shù)據(jù)庫(kù)蒿叠,可以快速轉(zhuǎn)化數(shù)據(jù)明垢,快速查詢數(shù)據(jù)呢?網(wǎng)上搜索一番后發(fā)現(xiàn)市咽,其實(shí)出發(fā)點(diǎn)有些問(wèn)題痊银,既然是數(shù)據(jù)庫(kù),肯定逃不過(guò)數(shù)據(jù)存儲(chǔ)魂务,有存儲(chǔ)就有延遲曼验。但我們的目標(biāo)是想盡可能削減甚至忽略這部分工作,把重心放在SQL查詢上粘姜。
換種說(shuō)法鬓照,內(nèi)存數(shù)據(jù)結(jié)構(gòu)或者集合可以看成存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)庫(kù),為什么非要找另一種存儲(chǔ)方式孤紧,且就單單想借用它的SQL查詢能力呢豺裆?
柳暗花明
在網(wǎng)上用各種關(guān)鍵詞搜索無(wú)果后,腦袋里突然冒出一個(gè)東西:Calcite号显。之前嘗試過(guò)用它做SQL解析臭猜,效果不是很好,并且在Hive中經(jīng)常能看到它的身影押蚤,一查果然有門(mén)道蔑歌。官網(wǎng)有這么一段描述,很有意思:
It contains many of the pieces that comprise a typical database management system, but omits some key functions: storage of data, algorithms to process data, and a repository for storing metadata.
Calcite intentionally stays out of the business of storing and processing data. As we shall see, this makes it an excellent choice for mediating between applications and one or more data storage locations and data processing engines. It is also a perfect foundation for building a database: just add data.
翻譯一下揽碘,大概意思就是:Calcite不同于傳統(tǒng)數(shù)據(jù)庫(kù)的點(diǎn)在于次屠,不存儲(chǔ)數(shù)據(jù)园匹,沒(méi)有數(shù)據(jù)處理的算法,不存儲(chǔ)元數(shù)據(jù)劫灶。它的作用是協(xié)調(diào)應(yīng)用與存儲(chǔ)在各處的數(shù)據(jù)裸违,僅僅通過(guò)添加數(shù)據(jù)就可以創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)。
Show me the code!
官網(wǎng)有很多例子本昏,特別是ReflectiveSchema和csv的例子供汛,是很好的引導(dǎo),其他各種數(shù)據(jù)庫(kù)引擎可以通過(guò)各種Adapter接入涌穆,下面我們實(shí)現(xiàn)一個(gè)二維數(shù)組的查詢怔昨,通過(guò)這個(gè)例子,大家可以簡(jiǎn)單改寫(xiě)實(shí)現(xiàn)各種數(shù)據(jù)結(jié)構(gòu)的查詢宿稀。
1.由上而下朱监,先實(shí)現(xiàn)數(shù)據(jù)庫(kù)Schema(可以忽略SchemaFactory),把source看作二維數(shù)組,meta看作元數(shù)據(jù)字段信息原叮。
public class MemorySchema extends AbstractSchema {
private Map<String, Table> tableMap;
private List<MemoryColumn> meta;
private List<List<Object>> source;
public MemorySchema(List<MemoryColumn> meta, List<List<Object>> source){
this.meta = meta;
this.source = source;
}
@Override
public Map<String, Table> getTableMap(){
if(CollectionUtils.isEmpty(tableMap)){
tableMap = new HashMap<>();
tableMap.put("memory", new MemoryTable(meta, source));
}
return tableMap;
}
}
2.實(shí)現(xiàn)Table赫编,模擬一張表
public class MemoryTable extends AbstractTable implements ScannableTable {
private List<MemoryColumn> meta;
private List<List<Object>> source;
public MemoryTable(List<MemoryColumn> meta, List<List<Object>> source){
this.meta = meta;
this.source = source;
}
@Override
public RelDataType getRowType(RelDataTypeFactory relDataTypeFactory) {
JavaTypeFactory typeFactory = (JavaTypeFactory) relDataTypeFactory;
//字段名
List<String> names = new ArrayList<>();
//類型
List<RelDataType> types = new ArrayList<>();
for(MemoryColumn col : meta){
names.add(col.getName());
RelDataType relDataType = typeFactory.createJavaType(col.getType());
relDataType = SqlTypeUtil.addCharsetAndCollation(relDataType, typeFactory);
types.add(relDataType);
}
return typeFactory.createStructType(Pair.zip(names,types));
}
@Override
public Enumerable<Object[]> scan(DataContext dataContext) {
return new AbstractEnumerable<Object[]>() {
@Override
public Enumerator<Object[]> enumerator() {
return new MemoryEnumerator(source);
}
};
}
}
3.實(shí)現(xiàn)Enumerator,它模擬一個(gè)迭代器奋隶,枚舉每一行數(shù)據(jù)
public class MemoryEnumerator implements Enumerator<Object[]> {
private List<List<Object>> source;
private int i = -1;
private int length;
public MemoryEnumerator(List<List<Object>> source){
this.source = source;
length = source.size();
}
@Override
public Object[] current() {
List<Object> list = source.get(i);
return list.toArray();
}
@Override
public boolean moveNext() {
if(i < length - 1){
i++;
return true;
}
return false;
}
@Override
public void reset() {
i = 0;
}
@Override
public void close() {
}
}
以上三部分就是核心擂送,Schema,Table和Enumerator唯欣。
總結(jié):MemorySchema需要實(shí)現(xiàn)getTableMap()方法嘹吨,它的作用是返回?cái)?shù)據(jù)庫(kù)中所有表:表名 -> 表的映射關(guān)系。
MemoryTable實(shí)現(xiàn)getRowType()方法境氢,返回字段信息蟀拷,也就是元數(shù)據(jù)信息。繼承最簡(jiǎn)單的ScannableTable萍聊,實(shí)現(xiàn)scan()方法问芬,返回枚舉器。
MemoryEnumerator繼承Enumerator寿桨,實(shí)現(xiàn)接口的各個(gè)方法即可此衅。有一點(diǎn)需要注意的是:按我們這種寫(xiě)法,i需要初始化為-1亭螟,初始化成0挡鞍,查詢到的數(shù)據(jù)會(huì)少一行。
補(bǔ)充類:
public class MemoryColumn<T> {
private String name;
private Class<T> type;
public MemoryColumn(String name, Class<T> type){
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public Class<T> getType() {
return type;
}
public void setType(Class<T> type) {
this.type = type;
}
public void setName(String name) {
this.name = name;
}
}
然后calcite-core的版本:1.20.0
驗(yàn)證
public class CalciteTest {
Properties info;
Connection connection;
Statement statement;
ResultSet resultSet;
public void getData(List<MemoryColumn> meta, List<List<Object>> source) throws SQLException {
// 構(gòu)造Schema
Schema memory = new MemorySchema(meta, source);
// 設(shè)置連接參數(shù)
info = new Properties();
info.setProperty(CalciteConnectionProperty.DEFAULT_NULL_COLLATION.camelName(), NullCollation.LAST.name());
info.setProperty(CalciteConnectionProperty.CASE_SENSITIVE.camelName(), "false");
// 建立連接
connection = DriverManager.getConnection("jdbc:calcite:", info);
// 執(zhí)行查詢
statement = connection.createStatement();
// 取得Calcite連接
CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
// 取得RootSchema RootSchema是所有Schema的父Schema
SchemaPlus rootSchema = calciteConnection.getRootSchema();
// 添加schema
rootSchema.add("memory", memory);
// 編寫(xiě)SQL
String sql = "select * from memory.memory where COALESCE (id, 0) <> 2 order by id asc";
resultSet = statement.executeQuery(sql);
while (resultSet.next()){
System.out.println(resultSet.getString(1)+":"+resultSet.getString(2)+":"+resultSet.getString(3));
}
resultSet.close();
statement.close();
connection.close();
}
public static void main(String[] args) throws SQLException {
List<MemoryColumn> meta = new ArrayList<>();
List<List<Object>> source = new ArrayList<>();
MemoryColumn id = new MemoryColumn("id", Long.class);
MemoryColumn name = new MemoryColumn("name", String.class);
MemoryColumn age = new MemoryColumn("age", Integer.class);
meta.add(id);meta.add(name);meta.add(age);
List<Object> line1 = new ArrayList<Object>(){
{
add(null);
add("a");
add(1);
}
};
List<Object> line2 = new ArrayList<Object>(){
{
add(2L);
add("b");
add(2);
}
};
List<Object> line3 = new ArrayList<Object>(){
{
add(3L);
add("c");
add(3);
}
};
List<Object> line4 = new ArrayList<Object>(){
{
add(null);
add("c");
add(4);
}
};
source.add(line1);source.add(line2);source.add(line4);source.add(line3);
new CalciteTest().getData(meta, source);
}
}
代碼本身不是很難预烙,有一點(diǎn)需要注意墨微,就是NullCollation.LAST.name()屬性,它可以使得排序時(shí)扁掸,null值總是被放到最后翘县,官網(wǎng)上還有其他3種衰琐。更多SQL語(yǔ)法詳見(jiàn)官網(wǎng)
另外一個(gè)比較關(guān)心的點(diǎn)是效率問(wèn)題,通過(guò)arthas監(jiān)控測(cè)試環(huán)境的服務(wù)炼蹦,測(cè)試5w數(shù)據(jù)時(shí),耗時(shí)小于97ms狸剃;1w數(shù)據(jù)時(shí)掐隐,耗時(shí)小于70ms;1000數(shù)據(jù)時(shí)钞馁,耗時(shí)20ms虑省。如果大家遇到類似問(wèn)題可以嘗試用一下,但效率問(wèn)題還是需要關(guān)注一下僧凰。