阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

聽著,我需要給你看一些可能會傷害你感情的東西:

CREATE TABLE MongoDB (
  _id  UUID PRIMARY KEY,
  data JSONB
);

在你評論區@我之前,先聽我說完。如果我告訴你,90% 的 NoSQL 用例都可以用這個資料庫管理員不想讓你知道的 Postgres 詭異技巧來處理,你會怎麼想?

問題:為什麼我們認為我們需要 NoSQL

我們都經歷過這種情況。凌晨兩點,你正忙於資料庫遷移,質疑人生中所有讓你走到這一步的選擇。你的產品經理只是要求“再加一個字段”,你就像回到 2009 年一樣,不停地寫遷移腳本。

「也許我應該只用 MongoDB,」你低聲自言自語。 “模式靈活!無需遷移!文件儲存!”

但問題是:你可能不需要 MongoDB。你需要 JSONB。

JSONB:我們不配擁有的英雄

JSONB 可不是隨便塞進 Postgres 欄位裡的 JSON。喔不,我的朋友。它是 JSON 更酷、更快、更吸引人的“老大哥”,它可是去健身房學習瞭如何使用索引的。

以下是 JSONB 的特別之處:

  • 二進位儲存格式(B 代表二進位,而不是🐝)

  • GIN 索引讓查詢變得非常快

  • 讓 JavaScript 開發者喜極而泣的原生運算符

  • 完整的 SQL 功能與 NoSQL 靈活性結合

這就像 MongoDB 和 Postgres 生了一個孩子,而這個孩子長大後成為了超級英雄。

大多數開發人員不知道的令人驚嘆的功能

改變你生活的運營商

-- The containment operator @>
-- "Does this JSON contain this structure?"
SELECT * FROM users
WHERE preferences @> '{"theme": "dark"}';

-- The existence operator ?
-- "Does this key exist?"
SELECT * FROM products
WHERE attributes ? 'wireless';

-- The arrow operators -> and ->>
-- -> returns JSON, ->> returns text
SELECT
  data->>'name' AS name,
  data->'address'->>'city' AS city
FROM users;

-- The path operator #>
-- Navigate deep into nested JSON
SELECT * FROM events
WHERE data #> '{user,settings,notifications}' = 'true';

索引特定的 JSON 路徑(等等,什麼?)

事情開始變得棘手了。你可以在 JSON 中的特定路徑上建立索引:

-- Index a specific field
CREATE INDEX idx_user_email ON users ((data->>'email'));

-- Index for existence queries
CREATE INDEX idx_attributes ON products USING GIN (attributes);

-- Index for containment queries
CREATE INDEX idx_preferences ON users USING GIN (preferences);

現在,您的 JSON 查詢比您的同事聲稱的「不需要索引,因為 MongoDB 可以處理」要快。

JSON 內部的全文搜尋

抓住你的鍵盤:

-- Add full-text search to JSON fields
CREATE INDEX idx_content_search ON articles
USING GIN (to_tsvector('english', data->>'content'));

-- Search like a boss
SELECT * FROM articles
WHERE to_tsvector('english', data->>'content') @@ plainto_tsquery('postgres jsonb amazing');

真實程式碼範例(主要內容)

讓我們從實際的事情開始。假設你正在建立一個 SaaS 產品(例如UserJot——我的回饋管理工具的無恥宣傳),並且需要儲存使用者偏好設定:

-- The hybrid approach: structured + flexible
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  preferences JSONB DEFAULT '{}',
  metadata JSONB DEFAULT '{}'
);

-- Insert a user with preferences
INSERT INTO users (email, preferences) VALUES (
  '[email protected]',
  '{
    "theme": "dark",
    "notifications": {
      "email": true,
      "push": false,
      "frequency": "daily"
    },
    "features": {
      "beta": true,
      "advancedAnalytics": false
    }
  }'
);

-- Query users who have dark theme AND email notifications
SELECT email FROM users
WHERE preferences @> '{"theme": "dark", "notifications": {"email": true}}';

-- Update nested preferences
UPDATE users
SET preferences = jsonb_set(
  preferences,
  '{notifications,push}',
  'true'
)
WHERE email = '[email protected]';

事件日誌模式(Chef's Kiss)

這是 JSONB 絕對閃耀的地方:

CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type TEXT NOT NULL,
  user_id UUID,
  occurred_at TIMESTAMPTZ DEFAULT NOW(),
  data JSONB NOT NULL
);

-- Index for fast event type + data queries
CREATE INDEX idx_events_type_data ON events (event_type)
WHERE event_type IN ('purchase', 'signup', 'feedback');
CREATE INDEX idx_events_data ON events USING GIN (data);

-- Insert different event types with different schemas
INSERT INTO events (event_type, user_id, data) VALUES
('signup', 'user-123', '{
  "source": "google",
  "campaign": "summer-2024",
  "referrer": "blog-post"
}'),
('purchase', 'user-123', '{
  "items": [
    {"sku": "PROD-1", "quantity": 2, "price": 49.99},
    {"sku": "PROD-2", "quantity": 1, "price": 19.99}
  ],
  "discount": "SUMMER20",
  "total": 99.97
}'),
('feedback', 'user-123', '{
  "type": "feature_request",
  "title": "Add dark mode",
  "priority": "high",
  "tags": ["ui", "accessibility"]
}');

-- Find all purchases with a specific discount
SELECT * FROM events
WHERE event_type = 'purchase'
AND data @> '{"discount": "SUMMER20"}';

-- Calculate total revenue from events
SELECT SUM((data->>'total')::NUMERIC) AS total_revenue
FROM events
WHERE event_type = 'purchase'
AND occurred_at >= NOW() - INTERVAL '30 days';

具有動態屬性的產品目錄

這個範例讓 MongoDB 開發人員質疑一切:

CREATE TABLE products (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  price NUMERIC(10,2) NOT NULL,
  attributes JSONB DEFAULT '{}'
);

-- Insert products with completely different attributes
INSERT INTO products (name, price, attributes) VALUES
('iPhone 15', 999.00, '{
  "brand": "Apple",
  "storage": "256GB",
  "color": "Blue",
  "5g": true,
  "screen": {
    "size": "6.1 inches",
    "type": "OLED",
    "resolution": "2532x1170"
  }
}'),
('Nike Air Max', 120.00, '{
  "brand": "Nike",
  "size": "10",
  "color": "Black/White",
  "material": "Mesh",
  "style": "Running"
}'),
('The Pragmatic Programmer', 39.99, '{
  "author": "David Thomas",
  "isbn": "978-0135957059",
  "pages": 352,
  "publisher": "Addison-Wesley",
  "edition": "2nd"
}');

-- Find all products with 5G
SELECT name, price FROM products WHERE attributes @> '{"5g": true}';

-- Find products by brand
SELECT * FROM products WHERE attributes->>'brand' = 'Apple';

-- Complex query: Find all products with screens larger than 6 inches
SELECT name, attributes->'screen'->>'size' AS screen_size
FROM products
WHERE (attributes->'screen'->>'size')::FLOAT > 6.0;

當 JSONB 徹底毀滅時(用例)

這裡是你絕對應該使用 JSONB 的地方:

  1. 使用者偏好/設定:每個使用者的需求都不一樣。不要建立 50 個布林值列。

  2. 事件日誌:不同的事件 = 不同的資料。 JSONB 處理起來非常得心應手。

  3. 產品目錄:書籍有 ISBN,鞋子有尺碼,手機有螢幕解析度。一個架構統領一切。

  4. API 回應快取:儲存第三方 API 回應而不對其進行解析。

  5. 表單提交:特別是當您建立像 UserJot 這樣的東西時,使用者回饋可以有自訂欄位。

  6. 功能標誌和配置

CREATE TABLE feature_flags (
  key TEXT PRIMARY KEY,
  config JSONB
);

INSERT INTO feature_flags VALUES
('new_dashboard', '{
  "enabled": true,
  "rollout_percentage": 25,
  "whitelist_users": ["user-123", "user-456"],
  "blacklist_countries": ["XX"],
  "start_date": "2024-01-01",
  "end_date": null
}');

劇情轉折:當你仍然需要真正的專欄時

讓我們先來現實一下。 JSONB 並不總是最佳選擇。以下情況應該使用常規列:

  • 外鍵:不能在外鍵約束中引用 JSONB 字段

  • 重度聚合:JSONB 欄位上的 SUM、AVG、COUNT 速度較慢

  • 頻繁更新:更新單一 JSONB 欄位會重寫整個 JSON

  • 類型安全:當你確實需要資料為整數時

秘訣?混合方法

CREATE TABLE orders (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),  -- Real FK
  total NUMERIC(10,2) NOT NULL,        -- For fast aggregations
  status TEXT NOT NULL,                -- For indexed lookups
  created_at TIMESTAMPTZ DEFAULT NOW(),
  line_items JSONB,                    -- Flexible item details
  metadata JSONB                       -- Everything else
);

大結局:遷移策略

以下是從 MongoDB 遷移到 Postgres/JSONB 的方法:

# Pseudo-code for the brave
import psycopg2
from pymongo import MongoClient

# Connect to both
mongo = MongoClient('mongodb://localhost:27017/')
postgres = psycopg2.connect("postgresql://...")

# Migrate with style
for doc in mongo.mydb.mycollection.find():
    postgres.execute(
        "INSERT INTO my_table (id, data) VALUES (%s, %s)",
        (str(doc['_id']), Json(doc))
    )

嘗試這個查詢並告訴我這不是魔術

這是你的作業。建立此表並執行此查詢:

-- Create a table
CREATE TABLE magic (
  id SERIAL PRIMARY KEY,
  data JSONB
);

-- Insert nested, complex data
INSERT INTO magic (data) VALUES
('{"user": {"name": "Alice", "scores": [10, 20, 30], "preferences": {"level": "expert"}}}'),
('{"user": {"name": "Bob", "scores": [5, 15, 25], "preferences": {"level": "beginner"}}}');

-- Mind-blowing query: Find users with average score > 15 AND expert level
SELECT
  data->'user'->>'name' AS name,
  (SELECT AVG(value::INT) FROM jsonb_array_elements_text(data->'user'->'scores') AS value) AS avg_score
FROM magic
WHERE data @> '{"user": {"preferences": {"level": "expert"}}}'
AND (
  SELECT AVG(value::INT)
  FROM jsonb_array_elements_text(data->'user'->'scores') AS value
) > 15;

如果這都不能讓你重新考慮你對 MongoDB 的依賴,我不知道還有什麼能做到。

獎勵:終極 JSONB 備忘單

-- Operators
@>  -- Contains
<@  -- Is contained by
?   -- Key exists
?|  -- Any key exists
?&  -- All keys exist
||  -- Concatenate
-   -- Delete key/element
#-  -- Delete at path

-- Functions
jsonb_set()           -- Update value at path
jsonb_insert()        -- Insert value at path
jsonb_strip_nulls()   -- Remove null values
jsonb_pretty()        -- Format for humans
jsonb_agg()          -- Aggregate into array
jsonb_object_agg()   -- Aggregate into object

-- Performance tips
1. Use GIN indexes for @> and ? operators
2. Use btree indexes for ->> on specific fields
3. Partial indexes for common queries
4. Don't nest more than 3-4 levels deep
5. Keep JSONB documents under 1MB

實話實說

聽著,我不是說 MongoDB 不好。它有自己的用武之地。但在你選擇單獨的 NoSQL 資料庫之前,先問問自己:JSONB 能做到嗎?

十有八九,答案是肯定的。而且,你還可以保留:

  • ACID事務

  • 當您需要時加入

  • 您現有的 Postgres 知識

  • 減少一個需要管理的資料庫

  • 口袋裡的錢(Postgres 是免費的!)

UserJot ,我們廣泛使用 JSONB 來儲存使用者回饋元資料、自訂欄位和整合配置。它既能提供類似 MongoDB 的彈性,還能提供 Postgres 的可靠性。兩者兼得。

現在開始@>所有東西吧!留言分享你最瘋狂的 JSONB 用例。我會在這裡回答你的問題,或許還會講更多資料庫相關的冷笑話。


附註:開頭那個 MongoDB 表?它真的很管用。我不是說你應該用它,但是…你可以用。 😈

P.S:如果你正在收集使用者回饋,並且想要比 JSONB 欄位更好的東西(雖然說實話,JSONB 也可以),請查看UserJot 。我們在底層使用了大量 JSONB 魔法來建造它。


原文出處:https://dev.to/shayy/i-replaced-mongodb-with-a-single-postgres-table-p0d


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!