1. 程式人生 > 實用技巧 >雲開發資料庫的高階查詢

雲開發資料庫的高階查詢

寫在前面:這是一篇工具文,如果沒有需要,不建議看完;如果有需要,可以隨時查詢內容。

高階的一些查詢,很多的資料是在查詢的時候就做完了,正常理論來說,資料庫是一定要對查詢優化到極致的,如果能夠將複雜的資料格式放到後臺來處理的話,會節省大量的時間。

除非說你能夠做到把業務處理的程式碼效能優化到極致的同時又讓它可讀性不差,並且易於更變,否則這種冗餘是可以接受的。

然後,我就直接用一些複雜查詢開場了蛤~


接下來的內容在Nodejs版本:10.15,雲開發sdk版本:~2.1.2下使用


先寫一套基本的程式碼,接下來的所有程式碼都需要把這段內容加到中間
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database();

// 後面兩個按需引入即可
const _ = db.command;
const $ = db.command.aggregate;

exports.main = async (event, context) => {
    // 內容區
}

一些詞法的基礎說明

先說兩個東西,project

group
這兩個東西,一個是對資料進行橫向的操作,一個則是對資料的縱向操作,這麼說可能不大明確,不過可以先看一下下面的一張表,而後聽我娓娓道來。

name age gender clan
Astroline 18 male Dragon
Eve 11 girl Arunoido

橫縱的資料就是那麼來的,橫向資料是Astroline, 18, male, Dragon;而縱向資料則是name, Astroline, Eve

project是處理單條資料的,而group是處理縱向資料的,多用於資料的彙總、歸類使用。

不管是project還是group,他們都是需要優先使用aggregate

的。

較大時間顆粒度查詢

一般來說,有些資料存在資料庫的時候並不理想,沒有規律,並不適合直接查詢,而project這個引數,則是對資料進行一次預處理將資料轉換為理想的資料。

這裡舉一個栗子:我想查詢某個月的訂單,但是我存入的時間格式為YYYY-mm-dd,而我的想法是查詢出某一個月的所有資料,很明顯這是一個非常困難的過程,我最初甚至是想在後臺迴圈輪詢月份的天數,然後把資料做整合。。。

這裡的思路是用字串操作將時間拆分為年、月、日不同的顆粒度;如果你用的是Datetime的形式儲存的,也可以使用小程式裡的時間操作工具做預處理,也是可以達到同樣的效果的。
return db.collection('order')
  .aggregate()  // 注意這裡哦,aggregate一定要加上,標記後面的查詢為聚合階段
  .project({
    price: true,
    quantity: true, 
    year: $.substr(['$create_time', 0, 4]),
    month: $.substr(['$create_time', 5, 2]),
    date: $.substr(['$create_time', 8, -1]),
  })
  // match也是聚合階段的方法,匹配的是`project`預處理後的結果
  .match({
    year: '2020',
    month: '04'
  })
  .end();  // 和基礎模板不同的,在aggregate裡只能用.end()結尾,返回的資料和get()有所出入。
  // .get()返回的是data: [{name: 'Astroline'}, {name: 'Eve'}];
  // .end()返回的是list: [{name: 'Astroline'}, {name: 'Eve'}];

注:$即是聚合操作符號,這裡使用了一個字串操作的方法,然後在方法裡面還有一個'$create_time',這一段匹配的是訂單裡的一個欄位,我這裡匹配的是建立時間。

當然,上面的程式碼還是有問題,一方面是查詢是死的,沒有動態的資料;另一方面,微信資料庫一次僅能查出來100條資料,所以需要做個拼接。

一套完整可用的程式碼貼在這裡 程式碼比較長,建議先跑一遍再理解 僅需要把表名和時間傳入即可使用,或者直接雲資料庫測試(需要刪掉`skip`、`limit`,並且修改變數為實際表名)
const { collection, date } = event;
const MAX_LIMIT = 100;

// 日期分組 0年 1月 2日
const date_time = date.split('-');  // 根據自己的資料格式調整
const tasks = [];

// 取出集合記錄總數 
const countResult = await db.collection(collection)
.aggregate()
.project({
    year: $.substr(['$create_time', 0, 4]),
    month: $.substr(['$create_time', 5, 2]),
})
.match({
    year: date_time[0],
    month: date_time[1]
})
.count('total')  // 聚合階段的count和基礎的count略微不同,返回的結果名稱要標記上
.end();

const total = countResult['list'][0]['total'];
const batchTimes = Math.ceil(total / 100);

for (let i = 0; i < batchTimes; i++) {
    const promise = db.collection(collection)
    .aggregate()
    .project({
        name: true,
        quantity: true,
        price: true,
        year: $.substr(['$create_time', 0, 4]),
        month: $.substr(['$create_time', 5, 2]),
    })
    .match({
        year: date_time[0],
        month: date_time[1]
    })
    .skip(i * MAX_LIMIT)
    .limit(MAX_LIMIT)
    .end();

    tasks.push(promise)
}

  // 等待所有
  return (await Promise.all(tasks)).reduce((acc, cur) => {
    return {
      list: acc.list.concat(cur.list),
      errMsg: acc.errMsg,
    }
  })

某月份訂單的的總價

然後業務就開始涉及到了一些彙總方面的內容了,因為彙總的內容不在小程式內部展示,於是我寫了一套外部的API,避免雲函式的運算過大(超時時間三秒鐘),導致的返回返回超時(其實超時時間可以修改,不過等三秒。。已經互動非常不友好了)

這次做的是一個某一個月份的訂單價格彙總,因為如果把業務丟在雲函式裡,計算是會非常龐大的(查詢速度不說,還要對查詢出來的結果重新遍歷)
  const {
    date,
  } = event;

// 日期分組 0年 1月 2日
const date_time = date.split('-');

return db.collection('order')
    .aggregate()
    .project({
        price: true,
        quantity: true,
        create_time: true,
        totalPrice: $.multiply(['$quantity', '$price']),
        year: $.substr(['$create_time', 0, 4]),
        month: $.substr(['$create_time', 5, 2]),
    })
    .match({
        year: date_time[0],
        month: date_time[1]
    })
    .group({
        _id: 'Eve!',
        quantity: $.sum('$quantity'),
        price: $.sum('$totalPrice'),
    })
    .end();

注:在matchgroup階段的資料都是基於project查出來的資料,舉個栗子,如果你要把project裡的quantity: true改成false的話,查出來的結果在進行group操作的時候quantity欄位找不到,就會返回為0

附:group支援的聚合操作

查詢資料分類

這裡就是要說到資料的分類了查詢查詢了,打個比方說,我需要查出今年的所有訂單,每個月要一個彙總(一般搞資料視覺化展示需要用到這種資料,咳)

我瞭解的有兩種分類方式,一種是建立一組歸類好的模板,然後用lookup拉外來鍵查詢,這種方式並不好,還需要額外建表,並且不夠靈活;而第二種,就是我下面要說的了。

在做資料視覺化,整理資料的時候我需要一組可以用在柱狀圖的資料,我不大想用後臺建立一堆`POJO`類,然後就乾脆放在了資料庫裡處理了...
  const {
    date,
  } = event;

// 日期分組 0年 1月 2日
const date_time = date.split('-');

return db.collection('order')
    .aggregate()
    .project({
        quantity: true,
        create_time: true,
        totalPrice: $.multiply(['$quantity', '$price']),
        year: $.substr(['$create_time', 0, 4]),
        month: $.substr(['$create_time', 5, 2]),
    })
    .match({
        year: date_time[0],
        month: date_time[1]
    })
    .group({
        _id: '$month',
        price: $.sum('$totalPrice'),
        create_time: $.first('$create_time'),
        quantity: $.sum('$quantity'),
    })
    .end();

條件分類查詢

當然,上一個業務提到的資料分類,我們還可以再改一下

比如說我加一點預處理的內容——

訂單有四種:未成交未付款、成交未付款、成交已付款、取消訂單(未成交且超時、訂單被取消)。按照一般查詢,我需要查詢四次才可以把資料查出來(`match({ type: 0 })、match({ type: 1 })、match({ type: 2 })、match({ type: 3 })`),而如果它是個橫向的資料(一條資料裡返回四種狀態),那對我來說是非常的舒服了。
  const {
    date,
  } = event;

// 日期分組 0年 1月 2日
const date_time = date.split('-');

return db.collection('order')
    .aggregate()
    .project({
        price: true,
        status: true,
        quantity: true,
        year: $.substr(['$created', 0, 4]),
        month: $.substr(['$created', 5, 2]),
        date: $.substr(['$created', 8, -1]),
        // 判斷條件
        uu: $.cond({  // Unsettled and unpaid 我想取名2u的,不過命名規範裡不可以數字打頭哦
            if: $.and([  // 這裡僅展示一些複合的條件判斷,一般訂單不會出現status的....不過我還是做了一個判斷,僅判斷可用訂單(1可用,0凍結)
                $.eq(['$type', 0]),  
                $.not($.eq(['$status', 0])) 
            ]),
            then: '$quantity',
            else: 0
        }),
        tp: $.cond({  // Transaction paid 成交已付款
             if: $.eq(['$type', 1]),
            then: '$quantity',
            else: 0
        }),
        tnp: $.cond({  // Transaction not paid 成交未付款
            if: $.eq(['$type', 2]),
            then: '$quantity',
            else: 0
        }),
        oo: $.cond({  // Outstanding orders 訂單已取消 2o
             if: $.eq(['$type', 3]),
            then: '$quantity',
            else: 0
        }),
    })
    .match({
        year: date_time[0],
        month: date_time[1]
    })
  .group({  // js、python等語言我習慣用下劃線,java、C#、ts一類的語言變數習慣用小駝峰,類名用大駝峰
        _id: 'Eve~',
        unsettled_and_unpaid: $.sum('$uu'),
        transaction_paid: $.sum('$tp'),
        transaction_not_paid: $.sum('$tnp'),
        outstanding_orders: $.sum('$oo')
  })
  .end()

最後,多表聯查案例

我很少寫電商類的程式,不過我還是知道電商裡面有兩個基本的表,商品類目以及商品表(專案體積若再大些,可能會拆出更細緻的表)。

一般來說,這方面的業務處理方式不會用lookup做的,除非資料不多(比如類目表上面還有一個商家表,美團這樣B2B的軟體,這樣就能很好的限制了查詢出來的商品數量,可以使用lookup查出所有關聯資料,換來極好的互動體驗)

這裡的場景模擬在使用者在小程式端,點選某個商家,進入時候檢視商品的查詢(雖然我寫的不是電商,不過還是能改成電商的查詢的,並且我相信用電商這個命題會更容易理解的吧)。我決定將lookup拆成兩部分來說,第一個部分是簡單的查詢,用於客戶的使用;另一個部分是用於企業領導層檢視的,做資料視覺化使用。

(其實我根本不需要寫基礎的lookup查詢,小程式官方文件裡已經寫的非常清楚了,主要是複雜的查詢我寫了一套出來)

有這麼三張表,店鋪表、類目表、商品表,關係為:店鋪和類目是1:m,類目和商品是1:m。

const { storeId } = event;

// 一般一個店鋪的類目不會超過100條的,所以大膽食用吧
return db.collection('category')
    .aggregate()
    .match({
        storeId: storeId
    })
    .lookup({
        from: 'product',
        localField: '_id',  // 小程式裡的預設唯一標識是 _id
        foreignField: 'category_id',
        as: 'productList'
    })
    .end()

進階的多表聯查

在lookup裡我依舊使用了很多複雜的處理,如pipeline、let變數,兩種查詢方式的權重是相同的,一條查詢裡不可能講明兩種方法,所以這裡單獨寫了一條。

對我而言,我覺得這種查詢除非是你想偷懶不寫後臺的業務處理,否則儘量不要寫這種程式碼....

有一個需求,上級想要看到一個銷售的柱狀圖資料...我真不想編各種各樣奇奇怪怪的需求了。。。饒了我吧QAQ

我們需要看到一個商家不同的商品的銷售狀況如何,做柱狀圖統計...
我真想直接把專案裡的查詢貼出來...但是寫的程式不允許我貼。。。只好再寫一套查詢用於部落格記錄...
const {  // 注意要傳值
    date,
    store_id
  } = event;

// 日期分組 0年 1月 2日
const date_time = date.split('-');

return db.collection('product')
    .aggregate()
    .lookup({
        from: 'order',
        let: {  // 變數宣告 引用的時候使用雙$符號 如'$$product_id'
        product_id: '$_id'
        },
        pipeline:
        $.pipeline()  // 流水處理 你可以直接理解pipeline就是正常查詢裡的,只不過它針對的物件是外鏈的表 .aggregate()
                .project({
                    quantity: true,
                    price: true,
                    create_time: true,
                    year: $.substr(['$create_time', 0, 4]),
                    month: $.substr(['$create_time', 5, 2]),
                })
                .match(_.expr(  // 這裡僅展示了一下lookup內的and操作符使用
                    $.and([
                        $.eq(['$product_id', '$$product_id']),  // 商品相同的內容
                        $.eq(['$year', date_time[0]]),  // 時間規定在月份
                        $.eq(['$year', date_time[1]]),  // 時間規定在月份
                    ])
                ))
                .group({
                    _id: 'Eve...',
                    quantity: $.sum('$quantity'),
                })
                .done(),  // pipeline需要用done結尾
        as: 'result'
    })
    .project({
        // 這個自己寫好啦
        name: true,
        type: true,
        create_time: true,
        sold: $.arrayElemAt(['$result', 0]),  
    })
    .match({
        store_id: store_id,
    })
    .end();

注:比較神奇的操作,lookup是可以巢狀的,也就是傳說中的傳說中三表聯查

該程式碼我沒有跑過測試,知道有這麼個東西即可,關於三表聯查的內容是可以搜到的

const { storeId } = event;

// 一般一個店鋪的類目不會超過100條的,所以大膽食用吧
return db.collection('store')
    .aggregate()
    .match({
        storeId: storeId
    })
    .lookup({
        from: 'category',
        localField: '_id',  // 小程式裡的預設唯一標識是 _id
        foreignField: 'store_id',
        as: 'categoryList'
    })
    .lookup({
        from: 'product',
        localField: '_id',  // 小程式裡的預設唯一標識是 _id
        foreignField: 'category_id',
        as: 'productList'
    })
    .end()

目錄跳轉:微信小程式雲開發資料庫查詢指南