Skip to content

Commit

Permalink
feat: 新增 MySQL 軟刪除與索引的實作筆記
Browse files Browse the repository at this point in the history
  • Loading branch information
marsen committed Aug 5, 2024
1 parent 96012f7 commit b19f190
Showing 1 changed file with 151 additions and 0 deletions.
151 changes: 151 additions & 0 deletions source/_posts/2024/mysql_partial_index_and_soft_delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: "[學習筆記] SQL 軟刪除與索引 (MySQL/MSSQL/PostgreSQL)"
date: 2024/08/05 13:53:10
---

## 前情提要

最近需要實現軟刪除功能,但在設計索引時遇到了一些問題。
商務情境如下,
有效的 ACCOUNT 必須是唯一的。
然而,帳戶有可能會被軟刪除,所以被刪除的 Account 可能會有多筆相同的資料。

### 表格設計

```sql
CREATE TABLE Demo_User (
Id SERIAL PRIMARY KEY,
Account VARCHAR(255) NOT NULL,
IsActive BOOLEAN DEFAULT TRUE
);
```

簡化設計的用戶表格 Demo_User,其中包含 ID、ACCOUNT 和 IsACTIVE 欄位。
依商業需求,當 IsActive 為 1 時,Account 必須唯一。
而且很有可能會有多筆相同的帳號被刪除,當 IsActive 為 0 時,Account 不會限制只有一筆(允許多筆)

後端工程師建議使用觸發器(Trigger)或在應用層(Backend)實現這個約束,
根據我的記憶,在微軟的 SQL Server 中,有一種稱為「條件約束」的設定,能夠針對特定條件創建索引。
可以大幅節省開發成本,我認為這類的功能不應該只有微軟專有,故作了一些搜尋後,特別以此篇記錄。

## 實作

我找到一個測試的[網站](https://sqlfiddle.com),你可以直接在這裡測試,不需要花費額外心力建立 SQL Server

### MySQL

```sql
-- 創建表格
CREATE TABLE Users (
Id INT AUTO_INCREMENT PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BOOLEAN DEFAULT FALSE
);

-- 設置 AUTO_INCREMENT 起始值為 1000
ALTER TABLE Users AUTO_INCREMENT = 1000;

-- 創建唯一索引,只針對 IsDeleted = FALSE 的行
CREATE UNIQUE INDEX unique_active_account ON Users ((CASE WHEN IsDeleted THEN Username END));
-- 插入數據
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user2', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user3', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制
INSERT INTO Users (Username, IsDeleted) VALUES ('user4', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user5', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 測試重複的 Username 插入,應該失敗
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 失敗,因為 user1 已經存在且 IsDeleted = FALSE
-- 測試重複的 Username 插入,但 IsDeleted = TRUE,應該成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 檢查插入結果
SELECT * FROM Users;

```

### PostgreSQL

```sql
-- 創建表
CREATE TABLE Users (
Id SERIAL PRIMARY KEY,
Username VARCHAR(255) NOT NULL,
IsDeleted BOOLEAN DEFAULT FALSE
);

ALTER SEQUENCE users_id_seq RESTART WITH 1000;
-- 創建部分索引,只針對 IsDeleted = FALSE 的行
CREATE UNIQUE INDEX unique_active_username ON Users (Username)
WHERE IsDeleted = FALSE;


-- 插入範例數據
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user2', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user3', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制
INSERT INTO Users (Username, IsDeleted) VALUES ('user4', FALSE); -- 成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user5', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 測試重複的 Username 插入,應該失敗
-- INSERT INTO Users (Username, IsDeleted) VALUES ('user1', FALSE); -- 失敗,因為 user1 已經存在且 IsDeleted = FALSE
-- 測試重複的 Username 插入,但 IsDeleted = TRUE,應該成功
INSERT INTO Users (Username, IsDeleted) VALUES ('user1', TRUE); -- 成功,因為 IsDeleted = TRUE 不受唯一索引限制

-- 檢查插入結果
SELECT * FROM Users;

```

### MSSQL

```sql
-- 設置正確的 SET 選項
SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;

-- 創建表
CREATE TABLE Demo_User (
Id INT IDENTITY(1000, 1) PRIMARY KEY,
Account VARCHAR(255) NOT NULL,
IsActive BIT DEFAULT 1
);

-- 創建篩選唯一索引
CREATE UNIQUE INDEX unique_active_account ON Demo_User(Account)
WHERE IsActive = 1;

-- 插入範例數據
INSERT INTO Demo_User (Account, IsActive) VALUES ('user1', 1); -- 成功
INSERT INTO Demo_User (Account, IsActive) VALUES ('user2', 1); -- 成功
INSERT INTO Demo_User (Account, IsActive) VALUES ('user3', 0); -- 成功,因為 IsActive = 0 不受唯一索引限制
INSERT INTO Demo_User (Account, IsActive) VALUES ('user4', 1); -- 成功
INSERT INTO Demo_User (Account, IsActive) VALUES ('user5', 0); -- 成功,因為 IsActive = 0 不受唯一索引限制

-- 測試重複的 Account 插入,應該失敗
-- INSERT INTO Demo_User (Account, IsActive) VALUES ('user1', 1); -- 失敗,因為 user1 已經存在且 IsActive = 1

-- 測試重複的 Account 插入,但 IsActive = 0,應該成功
INSERT INTO Demo_User (Account, IsActive) VALUES ('user1', 0); -- 成功,因為 IsActive = 0 不受唯一索引限制

-- 檢查插入結果
SELECT * FROM Demo_User;
```

## 小結

不太需要複雜的後端程式或是 DB Trigger,只需要在建立索引時加上條件,
特別注意 MySQL 的語法是使用 CASE WHEN,其他 DB 是使用 WHERE,
這與 DB 版本也有關係,使用前應該進一步去查詢官方文件。

另外關於遞增欄位,在不同的 DB 也有不同的實作方式。
實務上通常不用了解這麼多 DB 的差異,僅僅是我個人的好奇補充罷了,
業界主推還是 PostgreSQL,我個人不夠專業,但三種都略有碰過,最熟的還是 MSSQL。
僅為個人學習記錄,如果要在三者中擇一還是需要多方考慮自身的 Context 再作決定。

## 參考

- [MSSQL 唯一條件約束及檢查條件約束](https://learn.microsoft.com/zh-tw/sql/relational-databases/tables/unique-constraints-and-check-constraints?view=sql-server-ver16)
- [SQL Fiddle](https://sqlfiddle.com)

(fin)

0 comments on commit b19f190

Please sign in to comment.