主鍵的產生方式主要有三種:
一. 資料庫自動產生
二. GUID
三. 開發建立
嚴格講這三種產生方式有一定的交叉點,其定位方式將在下面進行講解。
第一種方式,主要將其定位在自增長的識別值種子:可以設定起始數值,及增長步長。其優點在於使用時完全將並發任務交於資料庫引擎管理,你不用擔心存在多使用者使用的時候會產生兩個相同的ID的情況。其缺點也在於此,多數的資料庫不提供直接擷取標識ID的方式,對於開發人員來說產生ID的方式是透明的,開發人員幾乎無法幹預此項。對於資料的遷移也不是很方便。
由於存在上面的利弊,這種自增長的ID一般多用於設計基礎資料表(系統啟動並執行基礎資訊,如員工表)主鍵,而極少(根本不)用於主從表主、外鍵,因為在產生主從表資料並關聯時,必須確定主表的ID,然後才能定位從表的關聯ID。
例(MsSQL): 複製代碼 代碼如下:--建立測試表
CREATE TABLE [Identity](
Id INT IDENTITY(1,2) NOT NULL PRIMARY KEY,--種子的起始值1,步長2
Number VARCHAR(20) UNIQUE NOT NULL,
Name VARCHAR(20) NOT NULL,
Password VARCHAR(20) DEFAULT(123),
Description VARCHAR(40) NULL
)
--插入記錄
INSERT INTO [Identity](Number,Name,Description) VALUES('001','1st','Id=1,因為起始值1')
INSERT INTO [Identity](Number,Name,Description) VALUES('002','2nd','Id=3,因為起始值1,步長2')
INSERT INTO [Identity](Number,Name,Description) VALUES('003','3rd','Id=5,由於字元長度超長,報錯插入失敗,造成此Id產生後被放棄')
INSERT INTO [Identity](Number,Name,Description) VALUES('004','4th','Id=7 not 5,因為第三條記錄插入失敗')
--檢索記錄,查看結果
SELECT * FROM [Identity]
結果:
(1 行受影響)
(1 行受影響)
訊息 8152,層級 16,狀態 14,第 3 行
將截斷字串或位元據。
語句已終止。
(1 行受影響)
(3 行受影響)
Id Number Name Password Description
1 001 1st 123 Id=1,因為起始值1
3 002 2nd 123 Id=3,因為起始值1,步長2
7 004 4th 123 Id=7 not 5,因為第三條記錄插入失敗
第二種方式,GUID即Globally Unique Identifier,也稱為UUID(Universally Unique IDentifier),全球唯一識別碼,GUID一般由32位十六進位的數值組成,其中包含網卡地址、時間及其他資訊。任何兩台電腦都不會產生相同的GUID,他的優點在唯一性,當需要資料庫整合時,能節約不少勞動力。比如總公司和分公司各自系統獨立運行,所有分公司資料定期需要提交到總部,可以避免合并資料時主鍵衝突問題,同時GUID還兼具自增長識別值種子特點,無需開發人員太多的關注。但是GUID資訊量大,佔用空間也大,關聯檢索時,估計效率上也不是很高,對於32位的十六進位其可讀性也差,雖然主鍵有對使用者的無意義性,但是在設計或者調試交流時很不方便。
從長遠考慮,為了保證資料的可移植性,一般還是會選擇使用GUID來作為主鍵。
例(MsSQL): 複製代碼 代碼如下:--建立測試表
CREATE TABLE GUID(
Id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,--當然你也可以用字串來儲存
Number VARCHAR(20) UNIQUE NOT NULL,
Name VARCHAR(20) NOT NULL,
Password VARCHAR(20) DEFAULT(123)
)
--插入記錄
INSERT INTO GUID(Id,Number,Name) VALUES(NewID(),'001','1st')
INSERT INTO GUID(Id,Number,Name) VALUES(NewID(),'002','2nd')
INSERT INTO GUID(Id,Number,Name) VALUES(NewID(),'003','3rd')
--檢索記錄,查看結果
SELECT * FROM GUID
結果:
Id Number Name Password
8E194F55-B4D3-4C85-8667-33BC6CD33BBC 001 1st 123
7141F202-7D0E-4992-9164-5043EC9FC6F6 002 2nd 123
E0E365A0-8748-4656-AF24-5D0B216D2095 003 3rd 123
第三種方式開發建立,其便捷性在於可控制性,此可控制性是指其組成形式,可以是整形、也可以是字元型,你可以根據實際情況給予多樣的組成及產生形式,說到這裡可能有的朋友就想起來自動產生單號,如:20120716001或者PI-201207-0001等等,沒錯,自我建立同樣適用於這些類似的應用。
說到自我建立,多數首先想到的是取Max(Id)+1,這種方式雖然省事,但是實際上對於定製(在生產單號之類的有一定意義的資訊時可能會有這樣的需求,主鍵沒必要)及並發的處理並不是很好。如,當前表中最大編號為1000,當C1和C2使用者同時取這個Id處理時,得到的都是1001,導致儲存失敗。常規的做法是在取值時候加鎖,但是當多使用者頻繁操作時,效能是個很大的問題,其中主要的原因之一是直接操作的業務資料表。
針對此種情況,解決方案是使用索引值表來儲存表名、當前或者下一個Id及其他資訊,如果系統中多個表Id都使用這種方式,那麼索引值表中就會有多條相應的規則記錄;當然也可以讓整個資料庫所有表的Id從都按相同的規則從一個源產生,那麼索引值表中只需要一條規則記錄即可。
下面來看看這樣一個使用索引值表例子的演變(MsSQL): 複製代碼 代碼如下:--建立索引值表
CREATE TABLE KeyTable(
ID INT IDENTITY(1,1) PRIMARY KEY NOT NULL,
TCode VARCHAR(20) UNIQUE NOT NULL,
TName VARCHAR(50) NOT NULL,
TKey INT NOT NULL,
)
GO
--插入測試記錄
INSERT INTO KeyTable(TCode,TName,TKey)
VALUES('T001','Test',0)
GO
--建立擷取指定表ID的預存程序,也可以修改成函數
CREATE PROCEDURE UP_NewTableID
@TCode VARCHAR(20),@NextID INT OUTPUT
AS
DECLARE @CurTKey INT,@NextTKey INT
BEGIN TRAN TransID
SELECT @CurTKey=TKey
FROM KeyTable
WHERE TCode = @TCode
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK TRAN TransID
RAISERROR('Warning: No such row is exists',16,1)
RETURN
END
SET @NextTKey = @CurTKey + 1
--WAITFOR DELAY '00:00:05'
UPDATE KeyTable
SET TKey = @NextTKey
WHERE TCode = @TCode
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK TRAN TransID
RAISERROR('Warning: No such row is updated',16,1)
RETURN
END
COMMIT TRAN TransID
SET @NextID = @NextTKey
GO
執行預存程序UP_NewTableID: 複製代碼 代碼如下:DECLARE @NextID INT
EXEC UP_NewTableID 'T001',@NextID OUTPUT
PRINT @NextID
啟動並執行時會發現很正常,擷取的結果也很正確。但是如果在高並發的情況,多個使用者可能就會擷取相同的ID,如果擷取的ID後是用於儲存對應表中的記錄,那麼最多隻有一個使用者能儲存成功。
下面類比一下並發情形,將上面的預存程序UP_NewTableID中語句WAITFOR DELAY '00:00:05'的注釋去掉,開啟3個查詢分析器的表單,依次執行上面語句。
預期是想分別獲得1,2,3,但是也許會發現多個表單的運行結果都是:1。這就是說在更新語句執行之前,大家都擷取的ID是0,所以下一個數值都是為1。(實際的數值,根據DELAY的參數大小及已耗用時間按間隔有關)
從這方面來分析的話有的朋友可能就會想到,是否可以在更新語句執行時判斷ID是不是原始ID了?修改過程: 複製代碼 代碼如下:ALTER PROCEDURE UP_NewTableID
@TCode VARCHAR(20),@NextID INT OUTPUT
AS
DECLARE @CurTKey INT,@NextTKey INT
BEGIN TRAN TransID
SELECT @CurTKey=TKey
FROM KeyTable
WHERE TCode=@TCode
IF @@ROWCOUNT=0BEGIN
ROLLBACK TRAN TransID
RAISERROR('Warning: No such row is exists',16,1)
RETURN
END
SET @NextTKey=@CurTKey+1
WAITFOR DELAY '00:00:05'
UPDATE KeyTable
SET TKey=@NextTKey
WHERE TCode=@TCode AND TKey=@CurTKey--此處加上TKey的校正
IF @@ROWCOUNT=0BEGIN
ROLLBACK TRAN TransID
RAISERROR('Warning: No such row is updated',16,1)
RETURN
END
COMMIT TRAN TransID
SET @NextID=@NextTKey
GO
如果開啟個3個執行過程來類比並發,那麼會有2個表單出現:
訊息 50000,層級 16,狀態 1,過程 UP_NewTableID,第 28 行
Warning: No such row is updated
由此會看到還是會由於並發導致有使用者操作失敗,但是較上一個至少將錯誤出現的時間點提前了。
那麼有沒有更好的方法,從查詢到更新結束整個事務過程中,不會有任何其他事務插入其中來攪局的辦法呢,答案很明確,有,使用鎖!需要選擇適當的鎖,否則效果將和上面的一樣。 複製代碼 代碼如下:ALTER PROCEDURE UP_NewTableID
@TCode VARCHAR(20),@NextID INT OUTPUT
AS
DECLARE @CurTKey INT,@NextTKey INT
BEGIN TRAN TransID
SELECT @CurTKey=TKey
FROM KeyTable WITH (UPDLOCK)--採用更新鎖定,並保持到事務完成
WHERE TCode=@TCode
IF @@ROWCOUNT=0BEGIN
ROLLBACK TRAN TransID
RAISERROR('Warning: No such row is exists',16,1)
RETURN
END
SET @NextTKey=@CurTKey+1
WAITFOR DELAY '00:00:05'
UPDATE KeyTable
SET TKey=@NextTKey
WHERE TCode=@TCode--此處無需驗證TKey是否與SELECT的相同
COMMIT TRAN TransID
SET @NextID=@NextTKey
GO
可以開啟N(N>=2)個表單來進行測試,將會看到所有操作都被序列化,結果就是我們想要的那樣。如此注釋或者去掉模仿並發的語句WAITFOR DELAY '00:00:05'即可。
如前面所說,這同樣適應於單據編號類似編碼的產生形式,只要對前面的代碼及索引值表稍作修改即可,有興趣的朋友可以一試。如果是從前端取得這個編號,並應用於各個記錄,那麼可能存在跳號的可能。如果為了保證不存在跳號,一種解決方案就是使用跳號表,將跳號記錄定期掃描並應用於其他記錄。另一種解決方案是將記錄的儲存操作放置到編號產生的過程中,形成一個序列化的事務。
俗話說蘿蔔白菜各有所愛,您用哪一種自有你的道理。