最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SQL高級(jí)特性實(shí)戰(zhàn)之窗口函數(shù)、JSONB與多數(shù)據(jù)庫(kù)兼容完全指南

 更新時(shí)間:2026年06月26日 10:39:29   作者:wei_shuo  
SQL中的JSON函數(shù)讓數(shù)據(jù)庫(kù)能直接解析、查詢和構(gòu)造JSON數(shù)據(jù),無需在應(yīng)用層反復(fù)序列化/反序列化,這篇文章主要介紹了SQL高級(jí)特性實(shí)戰(zhàn)之窗口函數(shù)、JSONB與多數(shù)據(jù)庫(kù)兼容的相關(guān)資料,需要的朋友可以參考下

為什么要聊 SQL 高級(jí)特性

做后端開發(fā)的都知道,日常跟數(shù)據(jù)庫(kù)打交道最多的就是寫 SQL。簡(jiǎn)單的增刪改查大家都會(huì),但真正把 SQL 用好其實(shí)有不少講究。我在項(xiàng)目中用 KES 數(shù)據(jù)庫(kù)做了不少?gòu)?fù)雜查詢,過程中發(fā)現(xiàn)它的 SQL 能力比我預(yù)期的要強(qiáng)很多,尤其是在窗口函數(shù)、JSON 數(shù)據(jù)處理和多數(shù)據(jù)庫(kù)兼容這幾個(gè)方面。這篇文章把實(shí)戰(zhàn)中積累的一些用法整理出來,希望能幫到正在寫復(fù)雜 SQL 的朋友。

一、數(shù)據(jù)類型與常用函數(shù)

很多開發(fā)者建表的時(shí)候習(xí)慣性地用 VARCHAR、INT、TIMESTAMP 這幾樣打天下,其實(shí)數(shù)據(jù)庫(kù)提供的數(shù)據(jù)類型遠(yuǎn)不止這些。選對(duì)數(shù)據(jù)類型不光能省存儲(chǔ)空間,還能讓查詢邏輯更簡(jiǎn)潔。

它支持的數(shù)據(jù)類型比較豐富,除了常見的數(shù)值型、字符型、日期型之外,還有一些值得關(guān)注的類型:

CREATE TABLE product_info (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(200) NOT NULL,
    tags        TEXT[],                    -- 數(shù)組類型
    attributes  JSONB,                     -- JSON 二進(jìn)制類型
    price       NUMERIC(10,2),
    status      SMALLINT DEFAULT 1,
    created_at  TIMESTAMP DEFAULT now(),
    geo_point   POINT                      -- 幾何點(diǎn)類型
);

數(shù)組類型在實(shí)際場(chǎng)景中挺有用的。比如商品標(biāo)簽、用戶角色列表這類一對(duì)多的關(guān)系,如果數(shù)據(jù)量不大且不需要單獨(dú)查詢,直接用一個(gè)數(shù)組字段存就夠了,省得再建一張關(guān)聯(lián)表。

字符串和日期函數(shù)

日常開發(fā)里字符串處理是高頻需求。這里的字符串函數(shù)跟標(biāo)準(zhǔn) SQL 比較接近,上手很快。

-- 字符串拼接,兩種方式都行
SELECT concat(first_name, ' ', last_name) AS full_name FROM users;
SELECT first_name || ' ' || last_name AS full_name FROM users;

-- 字符串截取和定位
SELECT substring(description FROM 1 FOR 100) AS brief FROM articles;
SELECT position('error' IN log_message) AS pos FROM app_logs;

-- 正則替換,批量清洗數(shù)據(jù)時(shí)很有用
SELECT regexp_replace(phone, '(\d{3})\d{4}(\d{4})', '\1****\2') AS masked_phone
FROM user_contacts;

-- 按分隔符拆分成多行
SELECT unnest(string_to_array('apple,banana,cherry', ',')) AS fruit;

日期函數(shù)也是寫得多了自然就熟了。幾個(gè)我經(jīng)常用的:

-- 日期加減
SELECT now() + INTERVAL '30 days' AS next_month;
SELECT created_at - INTERVAL '7 days' AS week_ago FROM orders;

-- 取日期部分
SELECT date_trunc('month', created_at) AS month_start,
       count(*) AS order_count
FROM orders
GROUP BY 1
ORDER BY 1;

-- 兩個(gè)日期的間隔
SELECT age(now(), created_at) AS account_age FROM users WHERE id = 1;

-- 提取星期幾、第幾周
SELECT extract(dow FROM now()) AS day_of_week,
       extract(week FROM now()) AS week_number;

date_trunc 這個(gè)函數(shù)做報(bào)表統(tǒng)計(jì)的時(shí)候特別好用,按月、按周、按天聚合數(shù)據(jù)都靠它。比在應(yīng)用層做日期格式化再分組要高效得多。

二、窗口函數(shù)與 LATERAL JOIN

窗口函數(shù)是我覺得 SQL 里最值得花時(shí)間學(xué)好的特性之一。它能在不改變結(jié)果集行數(shù)的前提下,對(duì)每一行執(zhí)行某種聚合或排名計(jì)算。聽起來抽象,看幾個(gè)例子就明白了。

排名類函數(shù)

-- 各部門內(nèi)按薪資排名
SELECT department_id,
       employee_name,
       salary,
       RANK() OVER w AS rank_num,
       DENSE_RANK() OVER w AS dense_rank_num,
       ROW_NUMBER() OVER w AS row_num
FROM employees
WINDOW w AS (PARTITION BY department_id ORDER BY salary DESC);

RANK 遇到相同值會(huì)并列排名然后跳號(hào),比如兩個(gè)并列第 2 之后就是第 4。DENSE_RANK 也并列但不跳號(hào),兩個(gè)并列第 2 之后是第 3。ROW_NUMBER 不并列,即使值相同也會(huì)給不同序號(hào)。選哪個(gè)取決于業(yè)務(wù)需求,做排行榜的時(shí)候一般用 DENSE_RANK,分頁(yè)的時(shí)候用 ROW_NUMBER。

WINDOW w AS 這種寫法是定義一個(gè)命名窗口,后面可以復(fù)用。如果多個(gè)窗口函數(shù)用相同的 PARTITION 和 ORDER BY 規(guī)則,這樣寫能少打很多字,也不容易出錯(cuò)。

偏移取值函數(shù)

LAG 和 LEAD 可以拿到當(dāng)前行前面或后面第 N 行的值,做同比環(huán)比分析的時(shí)候特別方便:

SELECT month,
       revenue,
       LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
       revenue - LAG(revenue, 1) OVER (ORDER BY month) AS diff,
       ROUND(
           (revenue - LAG(revenue, 1) OVER (ORDER BY month))::numeric
           / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0) * 100, 2
       ) AS growth_pct
FROM monthly_revenue;

這段 SQL 直接算出了每個(gè)月的營(yíng)收環(huán)比增長(zhǎng)率。NULLIF 用來防止除零錯(cuò)誤,當(dāng)上月營(yíng)收為零的時(shí)候返回 NULL 而不是報(bào)錯(cuò)。這種計(jì)算如果放到應(yīng)用層做,需要查出來再用循環(huán)處理,代碼量和性能都不如直接在 SQL 里搞定。

聚合窗口

窗口函數(shù)里也能用 SUM、AVG、COUNT 這些聚合函數(shù):

-- 計(jì)算累計(jì)銷售額(Running Total)
SELECT order_date,
       daily_total,
       SUM(daily_total) OVER (ORDER BY order_date) AS cumulative
FROM daily_sales;

-- 每個(gè)部門的薪資占比
SELECT employee_name,
       department_id,
       salary,
       ROUND(salary::numeric / SUM(salary) OVER (PARTITION BY department_id) * 100, 2) AS dept_pct
FROM employees;

累計(jì)求和在報(bào)表場(chǎng)景中很常見,比如財(cái)務(wù)上的累計(jì)回款、項(xiàng)目管理里的累計(jì)工時(shí)等等。以前這類需求通常是在應(yīng)用層做循環(huán)累加,現(xiàn)在一條 SQL 就能搞定。

LATERAL JOIN 是一個(gè)很多人不太熟悉但非常實(shí)用的特性。簡(jiǎn)單說,它允許 JOIN 右側(cè)的子查詢引用左側(cè)表的字段,實(shí)現(xiàn)一種"跨行關(guān)聯(lián)"的效果。普通的 JOIN 或者子查詢做不到這一點(diǎn),要么只能關(guān)聯(lián)外層查詢的字段(關(guān)聯(lián)子查詢),要么兩邊各自獨(dú)立查。

什么時(shí)候需要 LATERAL JOIN

最典型的場(chǎng)景就是"分組取 Top N"。比如查詢每個(gè)部門薪資最高的前 3 名員工:

SELECT d.department_name, e.employee_name, e.salary
FROM departments d
JOIN LATERAL (
    SELECT employee_name, salary
    FROM employees
    WHERE department_id = d.id
    ORDER BY salary DESC
    LIMIT 3
) e ON true;

如果沒有 LATERAL,這個(gè)需求用標(biāo)準(zhǔn) SQL 寫起來會(huì)非常別扭。要么用窗口函數(shù) ROW_NUMBER 套一層 CTE,要么寫一個(gè)很復(fù)雜的關(guān)聯(lián)子查詢。LATERAL JOIN 讓這種"對(duì)每一行執(zhí)行一次子查詢"的邏輯變得非常直觀。

與關(guān)聯(lián)子查詢的對(duì)比

關(guān)聯(lián)子查詢也能引用外層查詢的字段,但通常只能用在 SELECT 列表或者 WHERE 條件里,不能像 LATERAL 這樣作為 JOIN 的一部分返回多列多行。

-- 關(guān)聯(lián)子查詢寫法:只能在 SELECT 里返回單個(gè)值
SELECT d.department_name,
       (SELECT employee_name FROM employees
        WHERE department_id = d.id
        ORDER BY salary DESC LIMIT 1) AS top_employee
FROM departments d;

-- LATERAL JOIN 寫法:可以返回多列多行
SELECT d.department_name, e.employee_name, e.salary, e.hire_date
FROM departments d
JOIN LATERAL (
    SELECT employee_name, salary, hire_date
    FROM employees
    WHERE department_id = d.id
    ORDER BY salary DESC
    LIMIT 3
) e ON true;

關(guān)聯(lián)子查詢那個(gè)寫法只能拿到薪資最高的那一個(gè)人的名字,拿不到更多字段,也拿不到多條記錄。LATERAL JOIN 的限制就少很多,子查詢里想返回什么就返回什么,想返回幾行就返回幾行。

實(shí)際應(yīng)用場(chǎng)景

我在做訂單系統(tǒng)的時(shí)候遇到過一個(gè)需求:查詢每個(gè)用戶最近一筆訂單的詳情,包括訂單號(hào)、金額和下單時(shí)間。用 LATERAL JOIN 寫出來特別干凈:

SELECT u.username, u.phone, o.order_no, o.amount, o.created_at
FROM users u
LEFT JOIN LATERAL (
    SELECT order_no, amount, created_at
    FROM orders
    WHERE user_id = u.id
    ORDER BY created_at DESC
    LIMIT 1
) o ON true
WHERE u.status = 1;

這里用 LEFT JOIN LATERAL 而不是 JOIN LATERAL,是為了保證即使用戶沒有訂單也能出現(xiàn)在結(jié)果里(訂單相關(guān)的字段會(huì)是 NULL)。如果確定只查有訂單的用戶,直接用 JOIN LATERAL 就行。

還有一個(gè)場(chǎng)景是做"就近匹配"。比如物流系統(tǒng)里給每個(gè)倉(cāng)庫(kù)找最近的三個(gè)配送站:

SELECT w.warehouse_name, d.station_name, d.distance_km
FROM warehouses w
JOIN LATERAL (
    SELECT station_name,
           ROUND(
               (point(d.longitude, d.latitude) <-> point(w.longitude, w.latitude))::numeric, 2
           ) AS distance_km
    FROM delivery_stations d
    ORDER BY point(d.longitude, d.latitude) <-> point(w.longitude, w.latitude)
    LIMIT 3
) d ON true;

關(guān)于性能方面有一點(diǎn)需要注意:LATERAL 子查詢對(duì)外層表的每一行都會(huì)執(zhí)行一次,所以如果外層表行數(shù)很多,子查詢的執(zhí)行效率就很關(guān)鍵。建議在子查詢涉及的字段上建好索引,比如上面訂單那個(gè)例子,orders 表的 (user_id, created_at DESC) 上建復(fù)合索引,查詢速度會(huì)快很多。

三、CTE 與遞歸查詢

CTE(Common Table Expression)就是用 WITH 子句定義臨時(shí)結(jié)果集,讓復(fù)雜查詢變得更有層次感。我個(gè)人自從學(xué)會(huì) CTE 之后就很少寫嵌套子查詢了,因?yàn)閷訉忧短椎?SQL 讀起來太費(fèi)勁。

-- 用 CTE 拆分復(fù)雜邏輯
WITH active_users AS (
    SELECT id, username, last_login
    FROM users
    WHERE status = 1
      AND last_login > now() - INTERVAL '30 days'
),
user_orders AS (
    SELECT u.id AS user_id,
           u.username,
           count(o.id) AS order_count,
           sum(o.amount) AS total_amount
    FROM active_users u
    LEFT JOIN orders o ON o.user_id = u.id
    GROUP BY u.id, u.username
)
SELECT *
FROM user_orders
WHERE total_amount > 1000
ORDER BY total_amount DESC;

每一步做的事情一目了然,后續(xù)維護(hù)的人也容易看懂。我建議超過兩層嵌套的查詢都改用 CTE 重寫,代碼可讀性會(huì)有質(zhì)的提升。

遞歸 CTE 是另一個(gè)強(qiáng)大的特性,處理層級(jí)數(shù)據(jù)的時(shí)候特別有用。比如組織架構(gòu)樹、分類目錄樹這類場(chǎng)景:

-- 遞歸查某個(gè)節(jié)點(diǎn)的所有下級(jí)
WITH RECURSIVE dept_tree AS (
    -- 起點(diǎn):根節(jié)點(diǎn)
    SELECT id, name, parent_id, 1 AS level, name::text AS path
    FROM departments
    WHERE id = 1

    UNION ALL

    -- 遞歸:找下級(jí)
    SELECT d.id, d.name, d.parent_id, t.level + 1,
           t.path || ' > ' || d.name
    FROM departments d
    JOIN dept_tree t ON d.parent_id = t.id
)
SELECT * FROM dept_tree ORDER BY path;

level 字段記錄層級(jí)深度,path 字段拼出完整的層級(jí)路徑。如果擔(dān)心數(shù)據(jù)有循環(huán)引用導(dǎo)致無限遞歸,可以加一個(gè) WHERE t.level < 10 之類的限制。遞歸 CTE 的執(zhí)行效率比在應(yīng)用層遞歸查數(shù)據(jù)庫(kù)要高得多,因?yàn)橹唤换ヒ淮尉桶阉袑蛹?jí)數(shù)據(jù)拿回來了。

四、JSONB 數(shù)據(jù)處理

JSONB 是我在 KES 上用得最多的非關(guān)系型特性。很多場(chǎng)景下數(shù)據(jù)模型不夠確定,或者某些字段的屬性經(jīng)常變化,用 JSONB 存就非常靈活。

-- 插入 JSONB 數(shù)據(jù)
INSERT INTO user_profiles (user_id, profile) VALUES
(1, '{"name": "張三", "age": 28, "skills": ["Java", "Python", "SQL"], "address": {"city": "北京", "district": "朝陽(yáng)"}}'),
(2, '{"name": "李四", "age": 32, "skills": ["Go", "Rust"], "address": {"city": "上海", "district": "浦東"}}');

-- 提取單個(gè)字段
SELECT profile->>'name' AS name,
       (profile->'address'->>'city') AS city
FROM user_profiles;

-- 條件查詢(支持索引)
SELECT * FROM user_profiles
WHERE profile @> '{"address": {"city": "北京"}}';

-- 檢查數(shù)組是否包含某個(gè)元素
SELECT profile->>'name' AS name
FROM user_profiles
WHERE profile->'skills' ? 'Python';

-- 更新 JSONB 中的某個(gè)字段
UPDATE user_profiles
SET profile = jsonb_set(profile, '{age}', '29')
WHERE user_id = 1;

-- 刪除 JSONB 中的某個(gè) key
UPDATE user_profiles
SET profile = profile - 'address'
WHERE user_id = 2;

JSONB 相比 JSON 類型最大的好處是支持索引。對(duì)于經(jīng)常用來做查詢條件的 JSONB 字段,建一個(gè) GIN 索引能讓查詢速度快很多:

CREATE INDEX idx_profile_gin ON user_profiles USING GIN (profile);

不過要注意一點(diǎn),JSONB 雖然靈活但不能濫用。如果某個(gè)字段的查詢和統(tǒng)計(jì)非常頻繁,而且結(jié)構(gòu)是穩(wěn)定的,還是老老實(shí)實(shí)拆成普通列比較好。JSONB 最適合存那些"偶爾查一下"的擴(kuò)展屬性,比如用戶偏好設(shè)置、表單動(dòng)態(tài)字段之類的。

JSONB 聚合函數(shù)

除了基本的存取操作,JSONB 的聚合函數(shù)在實(shí)際開發(fā)中也特別實(shí)用,主要是 jsonb_agg 和 jsonb_object_agg 這兩個(gè)。

jsonb_agg 可以把多行數(shù)據(jù)聚合成一個(gè) JSON 數(shù)組,jsonb_object_agg 可以把鍵值對(duì)聚合成一個(gè) JSON 對(duì)象。這兩個(gè)函數(shù)在做 API 接口返回?cái)?shù)據(jù)的時(shí)候特別好用,能直接在 SQL 層面拼好前端需要的 JSON 結(jié)構(gòu),省掉應(yīng)用層的組裝邏輯。

-- 把每個(gè)部門的員工聚合成 JSON 數(shù)組
SELECT department_id,
       jsonb_agg(
           jsonb_build_object(
               'id', id,
               'name', employee_name,
               'salary', salary
           )
       ) AS employees
FROM employees
GROUP BY department_id;

-- 把配置項(xiàng)的 key-value 聚合成一個(gè) JSON 對(duì)象
SELECT jsonb_object_agg(config_key, config_value) AS settings
FROM app_config
WHERE app_name = 'order-service';

第二條查詢跑完之后返回的就是一個(gè)完整的 JSON 對(duì)象,類似 {“max_retry”: “3”, “timeout”: “30”, “enable_cache”: “true”} 這種結(jié)構(gòu),前端或者其他服務(wù)拿到就能直接用。

我之前做過一個(gè)用戶畫像的接口,需要把用戶基本信息、標(biāo)簽列表、最近訂單這些數(shù)據(jù)組裝成一個(gè)嵌套的 JSON 返回。一開始是在 Java 里查三次數(shù)據(jù)庫(kù)再拼裝,后來用 jsonb_agg 配合子查詢一條 SQL 就搞定了,響應(yīng)時(shí)間從 200ms 降到了 50ms 左右,效果非常明顯。

-- 組裝用戶完整畫像
SELECT jsonb_build_object(
    'user_id', u.id,
    'username', u.username,
    'tags', COALESCE(
        (SELECT jsonb_agg(t.tag_name)
         FROM user_tags t WHERE t.user_id = u.id), '[]'::jsonb
    ),
    'recent_orders', COALESCE(
        (SELECT jsonb_agg(
            jsonb_build_object(
                'order_no', o.order_no,
                'amount', o.amount,
                'created_at', to_char(o.created_at, 'YYYY-MM-DD')
            )
         )
         FROM (SELECT * FROM orders WHERE user_id = u.id
               ORDER BY created_at DESC LIMIT 5) o
        ), '[]'::jsonb
    )
) AS user_profile
FROM users u
WHERE u.id = 1024;

關(guān)系型數(shù)據(jù)與 JSON 的互轉(zhuǎn)

實(shí)際開發(fā)中經(jīng)常需要在關(guān)系型數(shù)據(jù)和 JSON 之間做轉(zhuǎn)換。有時(shí)候是把查詢結(jié)果轉(zhuǎn)成 JSON 給前端用,有時(shí)候是把 JSON 數(shù)據(jù)展開成關(guān)系表來查詢。

-- 關(guān)系表轉(zhuǎn) JSON:把查詢結(jié)果轉(zhuǎn)成 JSON 數(shù)組
SELECT jsonb_agg(to_jsonb(t))
FROM (
    SELECT id, product_name, price, stock
    FROM products
    WHERE category = '電子產(chǎn)品'
    ORDER BY price DESC
    LIMIT 10
) t;

-- JSON 轉(zhuǎn)關(guān)系表:把 JSON 數(shù)組展開成多行
SELECT item->>'name' AS product_name,
       (item->>'price')::numeric AS price,
       (item->>'quantity')::int AS quantity
FROM (
    SELECT jsonb_array_elements(order_items) AS item
    FROM orders
    WHERE order_no = 'ORD-2025-001'
) t;

-- row_to_json 把整行記錄轉(zhuǎn)成 JSON
SELECT row_to_json(t) FROM (
    SELECT id, username, email, created_at
    FROM users WHERE id = 1
) t;

row_to_json 和 to_jsonb 的區(qū)別在于前者返回 JSON 類型,后者返回 JSONB 類型。如果后續(xù)還需要對(duì)結(jié)果做 JSONB 的操作(比如用 @> 包含運(yùn)算符),建議直接用 to_jsonb。

實(shí)戰(zhàn):動(dòng)態(tài)表單數(shù)據(jù)存儲(chǔ)

JSONB 最讓我覺得物有所值的場(chǎng)景就是存儲(chǔ)動(dòng)態(tài)表單數(shù)據(jù)。我們之前做過一個(gè)工單系統(tǒng),不同類型的工單有不同的字段。IT 報(bào)修工單需要設(shè)備編號(hào)和故障類型,行政申請(qǐng)工單需要審批流程和預(yù)算編號(hào),人事變動(dòng)工單需要原部門和新部門。

如果給每種工單都建一張表,維護(hù)成本太高,而且后續(xù)新增工單類型還得再建表。我們的做法是把公共字段(工單號(hào)、類型、提交人、提交時(shí)間等)做成普通列,各類型特有的字段全部塞到一個(gè) JSONB 字段里:

CREATE TABLE work_orders (
    id            BIGSERIAL PRIMARY KEY,
    order_no      VARCHAR(50) NOT NULL,
    order_type    VARCHAR(30) NOT NULL,
    submitter_id  BIGINT NOT NULL,
    status        SMALLINT DEFAULT 0,
    extra_fields  JSONB DEFAULT '{}',
    created_at    TIMESTAMP DEFAULT now()
);

-- IT 報(bào)修工單
INSERT INTO work_orders (order_no, order_type, submitter_id, extra_fields)
VALUES ('WO-2025-001', 'IT_REPAIR', 101,
    '{"device_id": "PC-1024", "fault_type": "藍(lán)屏", "location": "3樓會(huì)議室", "urgency": "high"}');

-- 行政申請(qǐng)工單
INSERT INTO work_orders (order_no, order_type, submitter_id, extra_fields)
VALUES ('WO-2025-002', 'ADMIN_REQUEST', 205,
    '{"request_type": "辦公用品", "budget_code": "BUD-2025-Q2", "approval_chain": ["張經(jīng)理", "李總監(jiān)"], "estimated_cost": 1500}');

-- 按類型查詢,直接取 JSONB 里的字段
SELECT order_no,
       extra_fields->>'device_id' AS device_id,
       extra_fields->>'fault_type' AS fault_type
FROM work_orders
WHERE order_type = 'IT_REPAIR'
  AND extra_fields->>'urgency' = 'high';

這套方案用了一年多,整體效果不錯(cuò)。新增加工單類型的時(shí)候完全不用改表結(jié)構(gòu),只要在前端配好表單模板就行。寫入的時(shí)候就是普通的 INSERT,讀取的時(shí)候根據(jù)工單類型解析對(duì)應(yīng)的 JSONB 字段。當(dāng)然也有人質(zhì)疑過這種設(shè)計(jì)不夠"正規(guī)",但在業(yè)務(wù)變化快、字段不確定的階段,這種靈活性真的很重要。等后續(xù)業(yè)務(wù)穩(wěn)定了,再把高頻查詢的字段拆出來也不遲。

五、兼容模式的使用

這是我覺得做得比較貼心的一個(gè)功能。很多項(xiàng)目是從 Oracle 或者 MySQL 遷移過來的,SQL 語(yǔ)法和函數(shù)不完全一樣。如果逐條改寫 SQL 工作量很大,兼容模式可以解決大部分問題。

通過 db_compatibility 參數(shù)切換兼容模式:

-- 查看當(dāng)前兼容模式
SHOW db_compatibility;

-- 創(chuàng)建數(shù)據(jù)庫(kù)時(shí)指定兼容模式
CREATE DATABASE myapp_db WITH db_compatibility = 'oracle';
CREATE DATABASE myapp_db WITH db_compatibility = 'mysql';

Oracle 兼容模式

切到 Oracle 模式后,很多 Oracle 特有的語(yǔ)法和函數(shù)就能直接用了:

-- DUAL 表
SELECT sysdate FROM dual;

-- NVL 函數(shù)(等同于 COALESCE)
SELECT NVL(phone, '未填寫') FROM users;

-- DECODE 函數(shù)
SELECT DECODE(status, 1, '啟用', 0, '禁用', '未知') FROM accounts;

-- 字符串連接用 ||
SELECT '工號(hào): ' || emp_id || ' 姓名: ' || emp_name FROM employees;

-- ROWNUM 偽列(限制行數(shù))
SELECT * FROM orders WHERE ROWNUM <= 10;

-- TO_CHAR / TO_DATE
SELECT TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI:SS') FROM orders;
SELECT TO_DATE('2025-06-01', 'YYYY-MM-DD') + 30 FROM dual;

這些 Oracle 語(yǔ)法在遷移項(xiàng)目中可以大幅減少 SQL 改寫的工作量。不過兼容模式并不能覆蓋所有 Oracle 特性,一些高級(jí)特性(比如分析函數(shù)的特殊寫法、CONNECT BY 等)可能還需要手動(dòng)改寫。

MySQL 兼容模式

MySQL 模式下常見的 MySQL 風(fēng)格語(yǔ)法也基本支持:

-- IFNULL
SELECT IFNULL(nickname, username) AS display_name FROM users;

-- LIMIT 語(yǔ)法
SELECT * FROM products ORDER BY price DESC LIMIT 20 OFFSET 40;

-- GROUP_CONCAT(等同于 STRING_AGG)
SELECT department, GROUP_CONCAT(employee_name SEPARATOR ', ')
FROM employees
GROUP BY department;

-- 反引號(hào)引用標(biāo)識(shí)符
SELECT `order`.`id`, `user`.`name`
FROM `order`
JOIN `user` ON `order`.`user_id` = `user`.`id`;

需要注意的是,兼容模式是數(shù)據(jù)庫(kù)級(jí)別的設(shè)置,創(chuàng)建數(shù)據(jù)庫(kù)時(shí)指定,之后不能在線切換。所以在規(guī)劃階段就要確定好使用哪種兼容模式。如果你的應(yīng)用 SQL 是全新開發(fā)的,建議直接用默認(rèn)模式(標(biāo)準(zhǔn) SQL),這樣代碼更規(guī)范。

六、實(shí)用 SQL 技巧補(bǔ)充

最后分享幾個(gè)在實(shí)戰(zhàn)中積累的小技巧,單獨(dú)看都不復(fù)雜,但組合起來能解決不少問題。

UPSERT

INSERT INTO user_scores (user_id, score, updated_at)
VALUES (1, 95, now())
ON CONFLICT (user_id)
DO UPDATE SET score = EXCLUDED.score, updated_at = EXCLUDED.updated_at;

這個(gè)語(yǔ)法在做數(shù)據(jù)同步或冪等操作時(shí)非常好用。一條語(yǔ)句搞定"有就更新沒有就插入"的邏輯,不用先 SELECT 再判斷。

COALESCE 處理 NULL 值

-- 返回第一個(gè)非 NULL 的值
SELECT COALESCE(phone, email, '無聯(lián)系方式') AS contact FROM users;

-- 聚合時(shí)處理 NULL
SELECT department,
       COALESCE(SUM(bonus), 0) AS total_bonus
FROM salaries
GROUP BY department;

STRING_AGG 行轉(zhuǎn)列

-- 把多行拼成一個(gè)字符串
SELECT department,
       STRING_AGG(employee_name, ', ' ORDER BY employee_name) AS members
FROM employees
GROUP BY department;

EXISTS 替代 IN 做關(guān)聯(lián)查詢

-- 當(dāng)只需要判斷存在性時(shí),EXISTS 通常比 IN 更高效
SELECT u.username
FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.user_id = u.id
      AND o.created_at > now() - INTERVAL '7 days'
);

EXISTS 找到第一條匹配就返回,不需要把子查詢的結(jié)果集全部算出來,在子查詢結(jié)果較多的場(chǎng)景下性能優(yōu)勢(shì)明顯。

條件聚合

-- 一條 SQL 統(tǒng)計(jì)多個(gè)維度的數(shù)據(jù)
SELECT department,
       COUNT(*) AS total,
       COUNT(*) FILTER (WHERE status = 'active') AS active_count,
       COUNT(*) FILTER (WHERE salary > 20000) AS high_salary_count,
       AVG(salary) FILTER (WHERE status = 'active') AS avg_active_salary
FROM employees
GROUP BY department;

FILTER 子句是標(biāo)準(zhǔn) SQL 的寫法,比用 CASE WHEN 在聚合函數(shù)里做條件判斷更清晰。有些老版本的數(shù)據(jù)庫(kù)不支持 FILTER,可以用 SUM(CASE WHEN … THEN 1 ELSE 0 END) 來代替。

七、物化視圖與表分區(qū)

物化視圖的應(yīng)用

普通視圖(VIEW)只是一個(gè)保存下來的查詢定義,每次查詢視圖的時(shí)候,底層的 SQL 都會(huì)重新執(zhí)行一遍。當(dāng)數(shù)據(jù)量增長(zhǎng)到一定規(guī)模后,一些復(fù)雜的統(tǒng)計(jì)查詢跑起來會(huì)越來越慢。物化視圖(Materialized View)的思路是:把查詢結(jié)果實(shí)際存儲(chǔ)下來,后續(xù)查詢直接讀存儲(chǔ)的數(shù)據(jù),不用再跑一遍原始 SQL。

基本語(yǔ)法和用法

-- 創(chuàng)建物化視圖
CREATE MATERIALIZED VIEW mv_dashboard_stats AS
SELECT
    date_trunc('day', created_at) AS stat_date,
    count(*) AS order_count,
    sum(amount) AS total_amount,
    count(DISTINCT user_id) AS unique_buyers,
    avg(amount) AS avg_order_amount
FROM orders
WHERE status IN (1, 2, 3)
GROUP BY 1
ORDER BY 1;

-- 創(chuàng)建索引提升物化視圖的查詢速度
CREATE INDEX idx_mv_stats_date ON mv_dashboard_stats (stat_date);

創(chuàng)建的時(shí)候就把數(shù)據(jù)算好存下來了。后面查這個(gè)物化視圖就跟查普通表一樣快,因?yàn)樗举|(zhì)上就是在讀一張已經(jīng)計(jì)算好的表。

什么時(shí)候該用物化視圖

物化視圖最適合"查詢頻率高、源數(shù)據(jù)變化不頻繁"的場(chǎng)景。比如:

  • 儀表盤的統(tǒng)計(jì)面板,數(shù)據(jù)每小時(shí)或每天刷新一次就夠
  • 報(bào)表系統(tǒng)里的月度、季度匯總數(shù)據(jù)
  • 復(fù)雜的多表關(guān)聯(lián)查詢結(jié)果,供多個(gè)下游服務(wù)讀取

反過來,如果源數(shù)據(jù)一直在高頻變化,而且業(yè)務(wù)要求實(shí)時(shí)準(zhǔn)確,那物化視圖就不太合適了。因?yàn)槟憧吹降挠肋h(yuǎn)是上一次刷新時(shí)的快照。

刷新策略

物化視圖的數(shù)據(jù)不會(huì)自動(dòng)跟著源表變,需要手動(dòng)刷新:

-- 全量刷新(會(huì)鎖視圖,刷新期間不能查詢)
REFRESH MATERIALIZED VIEW mv_dashboard_stats;

-- 并發(fā)刷新(不鎖視圖,但要求物化視圖上有唯一索引)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dashboard_stats;

全量刷新簡(jiǎn)單粗暴,但刷新過程中視圖是被鎖住的,其他查詢會(huì)阻塞等待。并發(fā)刷新不會(huì)阻塞查詢,但前提條件是物化視圖上必須有唯一索引,不然會(huì)報(bào)錯(cuò):

-- 先建唯一索引才能用 CONCURRENTLY
CREATE UNIQUE INDEX idx_mv_stats_date_uniq
    ON mv_dashboard_stats (stat_date);

-- 然后就可以并發(fā)刷新了
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dashboard_stats;

生產(chǎn)環(huán)境建議都用 CONCURRENTLY,不然刷新的時(shí)候卡住一堆查詢就尷尬了。我之前踩過這個(gè)坑,用全量刷新一個(gè)統(tǒng)計(jì)視圖,刷新期間整個(gè)看板頁(yè)面都加載不出來,被運(yùn)維同事吐槽了好一陣。

刷新的時(shí)機(jī)一般靠定時(shí)任務(wù)來控制。可以用數(shù)據(jù)庫(kù)自帶的定時(shí)任務(wù),也可以在應(yīng)用層用 cron 或者調(diào)度框架來觸發(fā)。我一般會(huì)在業(yè)務(wù)低峰期做刷新,比如每天凌晨?jī)牲c(diǎn)刷一次前一天的統(tǒng)計(jì)數(shù)據(jù)。

存儲(chǔ)和性能的權(quán)衡

物化視圖本質(zhì)上是用存儲(chǔ)空間換查詢速度。一個(gè)復(fù)雜查詢?nèi)绻婕皫讖埓蟊淼亩啾黻P(guān)聯(lián)和聚合,原始查詢可能要跑 30 秒,但物化視圖查詢只要幾毫秒。代價(jià)是這份數(shù)據(jù)要額外占磁盤空間,而且需要定期維護(hù)刷新。

我的建議是:對(duì)于那些更新頻率不高但查詢頻率很高的統(tǒng)計(jì)數(shù)據(jù)(比如每天更新一次的看板數(shù)據(jù)),物化視圖的收益非常大。但對(duì)于實(shí)時(shí)性要求高的數(shù)據(jù),還是老老實(shí)實(shí)查原始表,或者在應(yīng)用層做緩存更合適。

表分區(qū)策略

當(dāng)單表數(shù)據(jù)量增長(zhǎng)到幾千萬(wàn)甚至上億條的時(shí)候,即使加了索引,查詢和維護(hù)的成本也會(huì)越來越高。表分區(qū)是應(yīng)對(duì)這種場(chǎng)景的一個(gè)利器。分區(qū)表把一個(gè)邏輯上的大表按照一定規(guī)則拆分成多個(gè)物理上的子表,但對(duì)應(yīng)用層來說還是一張表,SQL 寫法完全不用變。

按日期范圍分區(qū)

范圍分區(qū)(Range Partition)是最常用的分區(qū)方式,特別適合有時(shí)間維度的表,比如訂單表、日志表、交易流水表。

-- 創(chuàng)建分區(qū)表
CREATE TABLE orders (
    id          BIGSERIAL,
    order_no    VARCHAR(50) NOT NULL,
    user_id     BIGINT NOT NULL,
    amount      NUMERIC(12,2),
    status      SMALLINT DEFAULT 0,
    created_at  TIMESTAMP DEFAULT now()
) PARTITION BY RANGE (created_at);

-- 按月創(chuàng)建分區(qū)
CREATE TABLE orders_2025_01 PARTITION OF orders
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE orders_2025_02 PARTITION OF orders
    FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE TABLE orders_2025_03 PARTITION OF orders
    FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
-- ... 后續(xù)月份以此類推

PARTITION BY RANGE 指定按 created_at 做范圍分區(qū),每個(gè)分區(qū)覆蓋一個(gè)月的數(shù)據(jù)。FOR VALUES FROM … TO … 定義的是左閉右開區(qū)間,包含起始值但不包含結(jié)束值。

按類別列表分區(qū)

如果數(shù)據(jù)的分類比較固定,用列表分區(qū)(List Partition)更合適。比如按地區(qū)或按業(yè)務(wù)類型分區(qū):

CREATE TABLE sales_data (
    id          BIGSERIAL,
    region      VARCHAR(20) NOT NULL,
    product_id  BIGINT NOT NULL,
    amount      NUMERIC(12,2),
    sale_date   DATE DEFAULT CURRENT_DATE
) PARTITION BY LIST (region);

CREATE TABLE sales_east PARTITION OF sales_data
    FOR VALUES IN ('華東');
CREATE TABLE sales_south PARTITION OF sales_data
    FOR VALUES IN ('華南');
CREATE TABLE sales_north PARTITION OF sales_data
    FOR VALUES IN ('華北', '東北');
CREATE TABLE sales_west PARTITION OF sales_data
    FOR VALUES IN ('西南', '西北');

列表分區(qū)的好處是每個(gè)分區(qū)的數(shù)據(jù)量可以根據(jù)實(shí)際業(yè)務(wù)來定,不一定要均勻分配。比如華東和華南的銷售數(shù)據(jù)可能占大頭,華北和東北數(shù)據(jù)量差不多可以放一個(gè)分區(qū)里。

自動(dòng)創(chuàng)建分區(qū)

手動(dòng)每個(gè)月去建分區(qū)太麻煩了,可以寫一個(gè)函數(shù)自動(dòng)生成下個(gè)月的分區(qū):

CREATE OR REPLACE FUNCTION create_monthly_partition(target_month DATE)
RETURNS void AS $$
DECLARE
    partition_name TEXT;
    start_date TEXT;
    end_date TEXT;
BEGIN
    partition_name := 'orders_' || to_char(target_month, 'YYYY_MM');
    start_date := to_char(target_month, 'YYYY-MM-DD');
    end_date := to_char(target_month + INTERVAL '1 month', 'YYYY-MM-DD');

    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders FOR VALUES FROM (%L) TO (%L)',
        partition_name, start_date, end_date
    );

    RAISE NOTICE '分區(qū) % 已創(chuàng)建: [% ~ %)', partition_name, start_date, end_date;
END;
$$ LANGUAGE plpgsql;

-- 創(chuàng)建下個(gè)月的分區(qū)
SELECT create_monthly_partition(date_trunc('month', now()) + INTERVAL '1 month');

再配合定時(shí)任務(wù)每月初自動(dòng)執(zhí)行一下,就不用操心分區(qū)創(chuàng)建的事了。我之前有個(gè)項(xiàng)目就是因?yàn)橥颂崆敖ǚ謪^(qū),導(dǎo)致月初的 INSERT 直接報(bào)錯(cuò),數(shù)據(jù)寫不進(jìn)去了。后來加了自動(dòng)創(chuàng)建分區(qū)的定時(shí)任務(wù),才算徹底解決。這個(gè)教訓(xùn)告訴我,分區(qū)表的運(yùn)維自動(dòng)化一定要提前搞好。

分區(qū)裁剪與查詢性能

分區(qū)表最大的價(jià)值在于分區(qū)裁剪(Partition Pruning)。當(dāng)查詢的 WHERE 條件包含分區(qū)鍵時(shí),數(shù)據(jù)庫(kù)優(yōu)化器會(huì)自動(dòng)判斷只需要掃描哪些分區(qū),完全跳過不相關(guān)的分區(qū)。

-- 這條查詢只會(huì)掃描 orders_2025_03 這個(gè)分區(qū)
SELECT count(*), sum(amount)
FROM orders
WHERE created_at >= '2025-03-01'
  AND created_at < '2025-04-01';

-- 這條查詢會(huì)掃描 orders_2025_01 到 orders_2025_03 三個(gè)分區(qū)
SELECT date_trunc('month', created_at) AS month,
       count(*) AS order_count,
       sum(amount) AS total_amount
FROM orders
WHERE created_at >= '2025-01-01'
  AND created_at < '2025-04-01'
GROUP BY 1
ORDER BY 1;

可以用 EXPLAIN 來看執(zhí)行計(jì)劃,確認(rèn)分區(qū)裁剪是否生效:

EXPLAIN (ANALYZE, COSTS OFF)
SELECT count(*) FROM orders
WHERE created_at >= '2025-03-01' AND created_at < '2025-04-01';

如果執(zhí)行計(jì)劃里只出現(xiàn)了相關(guān)分區(qū)的掃描,說明裁剪生效了。如果出現(xiàn)了所有分區(qū)的掃描,那就得檢查一下 WHERE 條件里的分區(qū)鍵寫法是不是有問題。常見的坑是在分區(qū)鍵上套了函數(shù),比如 date_trunc(‘month’, created_at) = ‘2025-03-01’,這種寫法優(yōu)化器可能識(shí)別不出來,導(dǎo)致全分區(qū)掃描。

性能提升的幅度跟分區(qū)數(shù)量和數(shù)據(jù)量直接相關(guān)。我之前在一個(gè)日志表上做過對(duì)比,分區(qū)前全表掃描要 12 秒左右,分區(qū)后同樣查一個(gè)月的數(shù)據(jù)只要 0.3 秒,提升了差不多 40 倍。那個(gè)表總共有一年多的數(shù)據(jù),按月分成了 14 個(gè)分區(qū)。

使用分區(qū)表有幾點(diǎn)要注意:首先,主鍵或唯一約束必須包含分區(qū)鍵,不然數(shù)據(jù)庫(kù)無法保證跨分區(qū)的唯一性。其次,跨分區(qū)的 ORDER BY 和聚合操作需要合并多個(gè)分區(qū)的結(jié)果,性能提升沒那么明顯,最好還是在查詢條件里帶上分區(qū)鍵,把掃描范圍縮到盡量少的分區(qū)里。

小結(jié)

SQL 高級(jí)特性的價(jià)值在于把更多的數(shù)據(jù)處理邏輯下推到數(shù)據(jù)庫(kù)層面完成,減少應(yīng)用層的計(jì)算量和數(shù)據(jù)傳輸量。窗口函數(shù)、CTE、JSONB、LATERAL JOIN、物化視圖、表分區(qū)這幾個(gè)特性掌握好了,日常開發(fā)中能省掉不少代碼,也能讓系統(tǒng)性能上一個(gè)臺(tái)階。兼容模式在遷移項(xiàng)目中是個(gè)利器,但新項(xiàng)目還是建議用標(biāo)準(zhǔn) SQL 從頭寫。

寫 SQL 這件事,夠用和用好之間差距其實(shí)挺大的。多看看執(zhí)行計(jì)劃,多試試不同的寫法,慢慢就能寫出既簡(jiǎn)潔又高效的查詢了。KingbaseES 的 SQL 引擎在這些高級(jí)特性上的支持還是比較完整的,實(shí)際用下來沒有遇到太多坑。遇到問題的時(shí)候多翻翻官方文檔,很多細(xì)節(jié)在文檔里都有說明。

到此這篇關(guān)于SQL高級(jí)特性實(shí)戰(zhàn)之窗口函數(shù)、JSONB與多數(shù)據(jù)庫(kù)兼容完全指南的文章就介紹到這了,更多相關(guān)SQL窗口函數(shù)、JSONB與多數(shù)據(jù)庫(kù)兼容內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

临武县| 偃师市| 海南省| 青神县| 新绛县| 无为县| 佛冈县| 河曲县| 罗田县| 调兵山市| 隆林| 眉山市| 牟定县| 陕西省| 宁城县| 青龙| 昌乐县| 兴国县| 太仓市| 七台河市| 海安县| 叶城县| 西和县| 宁晋县| 仪陇县| 沂水县| 会同县| 阳谷县| 泗水县| 建阳市| 和静县| 黄浦区| 色达县| 乐清市| 扎囊县| 恩平市| 乌鲁木齐市| 井陉县| 新安县| 宜川县| 临汾市|