利用Calcite做內(nèi)存查詢

問(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)注一下僧凰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末探颈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子训措,更是在濱河造成了極大的恐慌伪节,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绩鸣,死亡現(xiàn)場(chǎng)離奇詭異怀大,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)呀闻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)化借,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人捡多,你說(shuō)我怎么就攤上這事蓖康。” “怎么了垒手?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵蒜焊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我科贬,道長(zhǎng)山涡,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任唆迁,我火速辦了婚禮鸭丛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唐责。我一直安慰自己鳞溉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布鼠哥。 她就那樣靜靜地躺著熟菲,像睡著了一般看政。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抄罕,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天允蚣,我揣著相機(jī)與錄音,去河邊找鬼呆贿。 笑死嚷兔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的做入。 我是一名探鬼主播冒晰,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼竟块!你這毒婦竟也來(lái)了壶运?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤浪秘,失蹤者是張志新(化名)和其女友劉穎蒋情,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體耸携,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡恕出,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了违帆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浙巫。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖刷后,靈堂內(nèi)的尸體忽然破棺而出的畴,到底是詐尸還是另有隱情,我是刑警寧澤尝胆,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布丧裁,位于F島的核電站,受9級(jí)特大地震影響含衔,放射性物質(zhì)發(fā)生泄漏煎娇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一贪染、第九天 我趴在偏房一處隱蔽的房頂上張望缓呛。 院中可真熱鬧,春花似錦杭隙、人聲如沸哟绊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)票髓。三九已至攀涵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洽沟,已是汗流浹背以故。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留裆操,地道東北人怒详。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像跷车,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子橱野,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容