惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
Engineering at Meta
Engineering at Meta
量子位
A
About on SuperTechFans
阮一峰的网络日志
阮一峰的网络日志
Recent Announcements
Recent Announcements
博客园 - 司徒正美
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
腾讯CDC
Jina AI
Jina AI
C
Check Point Blog
H
Help Net Security
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
爱范儿
爱范儿
I
InfoQ

Nic Lin's Blog

謝明真 - 高效領導力的課後筆記 NFT 開發實戰!基礎智能合約入門 (3) NFT 開發實戰!基礎智能合約入門 (2) NFT 開發實戰!基礎智能合約入門 (1) 如何自我檢測 log4j CVE 漏洞 Rails 如何在資料寫入時記錄來源 IP 位置 如何經營工程師 Youtube 頻道 - Part 8 營收篇 如何經營工程師 Youtube 頻道 - Part 7 酸民文化篇 如何經營工程師 Youtube 頻道 - Part 5 設備器材篇 如何經營工程師 Youtube 頻道 - Part 4 後製剪輯篇 如何經營工程師 Youtube 頻道 - Part 3 文案企劃篇 如何經營工程師 Youtube 頻道 - Part 2 設備器材篇 如何經營工程師 Youtube 頻道 - Part 1 制訂頻道方向篇 如何經營工程師 Youtube 頻道 - Part 0 Rails 中避免 race condition 的最佳實踐(二) Rails 中避免 race condition 的最佳實踐(一) 10 分鐘整合 google sheet 做自動化開發功能週報 經營 Side Project 300 天所帶來的收穫及挑戰 我的 Youtube 影片製作流程 API 設計時必須注意的 HTTP header 底線問題 如何提升你的程式可讀性之實務技巧(三) 如何提升你的程式可讀性之實務技巧(二) 如何提升你的程式可讀性之實務技巧(一) Ruby 中使用 freeze 優化效能的時機 避免 React 中的 useEffect 無限 render 在 Rails 內輕量使用 Vue Component 的最佳實踐 如何在區域網路用 Docker 架設有 SSL 的 Gitlab 從被問到問人,那些我常問的面試問題 [Rails] 如何漂亮寫出可維護的 query (Maintainable Rails Query) 在已知長度情況下優化 slice 的性能
在 PostgreSQL 下如何漂亮的拿到兩個欄位時間差的平均
Nic Lin · 2018-05-16 · via Nic Lin's Blog

有個需求是,對一個集合算出所有的數據中,兩個欄位的時間相減,取全部平均花費時間。

狀況如下

# == Schema Information
#
# Table name: orders
# ...
#  notify_at       :datetime
#  released_at   :datetime
# ...

我們希望可以拿到訂單中,每一筆出貨時間與通知轉帳時間相減後的秒數,除以所有訂單

這樣一來,我就可以知道訂單平均在這個階段「付款 -> 出貨」所需花費時間的平均值

如果寫純 SQL query 當然不難

SELECT AVG(orders.released_at - orders.notify_at) FROM orders

但在 Rails 中,要如何對已有的大量數據做這樣的計算呢?

非正規化

  1. 對 orders 新增一個欄位 processing_time
  2. 在 order 的 after_commit callback 中計算並更新這個時間
  3. 寫 task 對以前的訂單進行 patching
  4. 最後你就可以用 Order.average(:processing_time)

會遇到幾個問題

  1. 每次訂單完成時,都會更新這個欄位,多一條 query
  2. 如果資料量龐大,task 會跑很久,也會在這個時間吃滿 db memory

寫純 SQL Query 在 model 內

也不是不行,但如果團隊都是 ORM 派的就很受不了了 XD

class Order < ApplicationRecord
  def self.avg_released_time
    sql = <<-SQL
    SELECT AVG(orders.released_at - orders.notify_at) FROM orders
    SQL
    
    find_by_sql(sql)
  end
end

# 拿到這個...也不能用
# [
#    [0] #<Order:0x00007fe0c7d16370> {
#        :id => nil
#    }
# ]

更好的作法

既然有 average (ActiveRecord::Calculations) 可以用,那只要組合一下就行了

一開始會想 Order.average(:xxx), 這樣到底要怎麼寫?

xxx 裡面到底要放什麼?

能夠跟 where 一樣帶參數之類的嗎?

看了一下 source code 後發現,他其實是去呼叫 calculate

# File activerecord/lib/active_record/relation/calculations.rb, line 55
    def average(column_name, options = {})
      # TODO: Remove options argument as soon we remove support to
      # activerecord-deprecated_finders.
      calculate(:average, column_name, options)
    end

於是我嘗試出了這樣的組合

Order.calculate(:average, "orders.notify_at - orders.created_at")
# (0.7ms)  SELECT AVG(orders.notify_at - orders.created_at) FROM "orders"
# 0.0

發現好像成功了,但不知道為什麼數據總是 0.0

後來發現這樣的 timestamp 相減出來的數值是時間格式,但 average 這個 API 是預計回傳 Numeric, 所以就會無論如何都回傳 0.0

那麼就要用 PostgreSQL 的方法,把兩筆時間相減後變成數字,這樣 Rails 應該就接的到了

找了一下方法

epoch 可以將時間轉換成為秒數

SELECT EXTRACT(EPOCH from TIMESTAMP '2001-02-16 20:38:40');
Result: 982352320

SELECT EXTRACT(EPOCH from INTERVAL '5 days 3 hours');
Result: 442800

那麼把這個方法用 Rails 呼叫看看

Order.calculate(:average, "extract(epoch from orders.notify_at - orders.created_at)")
#   (1.3ms)  SELECT AVG(extract(epoch from orders.notify_at - orders.created_at)) FROM "orders"
# 51.146269

完美,這樣一來不用新增欄位,也可以快速的拉出數據,兼顧效能及美觀

稍微整理一下就可以變成一個好用的方法

class Order < ApplicationRecord
  def self.avg_released_time
    calculate(:average, "extract(epoch from orders.released_at - orders.notify_at)") || 0
  end
end

# Usage
# Order.avg_released_time

參考來源: