Uniqueidentifier (transact-sql)

GUID as Primary Key

The idea is to create a unique value e.g a new productId, on one of the application layers without performing a round-trip to the database in order to ask for a new Id. So, the generated GUID value becomes a PK value for the new product in e.g Products table. The PK may be implemented as a unique non-clustered index. The index is likely to be highly fragmented since the generated GUIDs are completely random. Also, keep in mind that the PK is highly likely to be a part of one or more referential integrity constraints(Foreign Keys) in tables like e.g ProductInventory, ProductListPriceHistory, etc. Moreover, the Foreign keys may be, at the same time, part of the composite PKs on the foreign tables – Figure 7. This design may have a negative effect on many tables and the database performance in general.

An alternative approach may be to define GUID column as an Alternate Key* enforced by a unique NCI and to use INT or BIGINT along with the IDENTITY property or a Sequencer as a surrogate PK. The key can be enforced by the unique clustered index. This way we can avoid excessive fragmentation and enforce referential integrity in a more optimal way – Figure 7,rowguid column.
Figure 7, GUID column as an Alternate Key – Adventure Works

*Alternate Key represents column(s) that uniquely identify rows in a table. A table can have more than one column or combinations of columns that can uniquely identify every row in that table. Only one choice can be set as the PK. All other options are called Alternate Keys.

GUID values can be created by SQL Server during the INSERT operations.  E.g Client code constructs a new product (product name, description, weight, color, etc..) and INSERTs the information(a new row) into the Products table. The NEWID() fn automatically creates and assigns a GUID value to the new row through a DEFAULT constraint on e.g ProductId column. Client code can also generate and supply GUID for the new product. The two methods can be mixed since the generated GUIDs are globally unique.

What I often see in different production environments is that the GUID values are used as PK values even if there is no need for the globally unique values.
Very few of them had better security in mind i.e  It is safer to expose a GUID than a numeric value when querying DB through a public API. The exposed numeric value in the URL may potentially be used to harm the system. E.g http://myApp/productid/88765 can suggest that there is productId =88764 etc., but with a GUID value, these guesses will not be possible – Figure 7, data access point.

In most DB designs, at least in the ones I’ve had an opportunity to work on,  GUIDs are used only because it was convenient from the application code design perspective.

When the application and the database become larger and more complex, these early decisions can cause performance problems. Usually, these problems are solved by, so-called quick fixes/wins. As the rule of thumb, the first “victim” of those “wins” is always data integrity e.g adding NOLOCK table hints everywhere, removing referential integrity(FK), replacing INNER JOINS with LEFT JOINS, etc. This inevitably leads to a new set of bugs that are not easy to detect and fix. This last paragraph may be too much, but this is what I am seeing in the industry.
Use GUIDs with caution and with the cost-benefit in mind

SQL Server, SSIS и Biml типы данных

Таблица ниже является упрощенной схемой связи между типами данныхSQL Server, SSIS и Biml.
Таблица не включает все возможные комбинации и все виды типов данных, но полезна как быстрая ссылка при разработке и изучении Biml.

SQL Server SSIS Variables SSIS Pipeline Buffer OLE DB ADO.NET Biml
bigint Int64 DT_I8 LARGE_INTEGER Int64 Int64
binary Object DT_BYTES Binary Binary
bit Boolean DT_BOOL VARIANT_BOOL Boolean Boolean
char String DT_STR VARCHAR StringFixedLength AnsiStringFixedLength
date Object DT_DBDATE DBDATE Date Date
datetime DateTime DT_DBTIMESTAMP DATE DateTime DateTime
datetime2 Object DT_DBTIMESTAMP2 DBTIME2 DateTime2 DateTime2
datetimeoffset Object DT_DBTIMESTAMPOFFSET DBTIMESTAMPOFFSET DateTimeOffset DateTimeOffset
decimal Decimal DT_NUMERIC NUMERIC Decimal Decimal
float Double DT_R8 FLOAT Double Double
geography DT_IMAGE Object Object
geometry DT_IMAGE Object Object
hierarchyid DT_BYTES Object Object
image (*) Object DT_IMAGE Binary Binary
int Int32 DT_I4 LONG Int32 Int32
money Object DT_CY, DT_NUMERIC CURRENCY Currency Currency
nchar String DT_WSTR NVARCHAR StringFixedLength StringFixedLength
ntext (*) String DT_NTEXT String String
numeric Decimal DT_NUMERIC NUMERIC Decimal Decimal
nvarchar String DT_WSTR NVARCHAR String String
nvarchar(max) Object DT_NTEXT String
real Single DT_R4 FLOAT, DOUBLE Single Single
rowversion Object DT_BYTES Binary Binary
smalldatetime DateTime DT_DBTIMESTAMP DATE DateTime DateTime
smallint Int16 DT_I2 SHORT Int16 Int16
smallmoney Object DT_CY, DT_NUMERIC CURRENCY Currency Currency
sql_variant Object DT_WSTR, DT_NTEXT Object Object
table Object
text (*) Object DT_TEXT AnsiString
time Object DT_DBTIME2 DBTIME2 Time Time
timestamp (*) Object DT_BYTES Binary Binary
tinyint Byte DT_UI1 BYTE Byte Byte
uniqueidentifier String, Object DT_GUID GUID Guid Guid
varbinary Object DT_BYTES Binary Binary
varbinary(max) Object DT_IMAGE Binary Binary
varchar String DT_STR VARCHAR String AnsiString
varchar(max) Object DT_TEXT AnsiString
xml Object DT_NTEXT Xml

(* Данные типы данных будут удалены в будущих версиях SQL Server.
Избегайте использование этих типов данных в новых проектах и, по возможности, измените их в текущих проектах.)

GUID as PK and the Clustered index key

Sometimes developers decide to use GUID values as a PK enforced by the clustered index. This means that the primary key column is at the same time the clustered index key. Data pages(leaf level) of a clustered index are logically ordered by the clustered index key values.
One of the reasons for this design may be the ability to easily merge data from different databases in the distributed database environment. The same idea can be implemented more efficiently using GUID as an alternative key as explained earlier.
More often, the design is inherited from SQL Server’s default behavior when the PK is created and automatically implemented as the clustered index key unless otherwise specified.

Using GUID as clustered index key leads to extensive page and index fragmentation. This is due to its randomness. E.g every time the client app inserts a new Product, a new row must be placed in a specific position i.e specific memory location on a data page. This is to maintain the logical order of the key values. The pages(nodes) are part of a doubly linked list data structure.  If there is not enough space on the designated page for the new row, the page must be split into two pages to make the necessary space for the new row. The physical position of the newly allocated page (8KB memory space) in the data file does not follow the order of the index key (it is not physically next to the original page). This is known as logical fragmentation. Splitting data pages introduces yet another type of fragmentation, physical fragmentation which defines the negative effect of the wasted space per page after the split. The increased number of “half full” pages along with the process of splitting the pages has a negative impact on query performance.

The “potential collateral damage” of the decision to use GUID as clustered index key is non-unique non-clustered indexes.
A non-clustered index that is built on a clustered index,  at the leaf level, contains row locators- the clustered index key values. These unique values are used as pointers to the clustered index structure and the actual rows – more information can be found here – The data Access Pattern section.
A non-unique NCI can have many duplicate index key values. Each of the key values is “coupled” with a unique pointer – in this case, a GUID value. Since GUID values are random the new rows can be inserted in any position within the range of the duplicated values. This introduces the fragmentation of the NCI. The more duplicated values, the more fragmentation.

The fragmentation can be “postponed” by using the FILLFACTOR setting. The setting instructs SQL Server what percentage of each data page should be used to store data. The “extra” free space per page can “delay” page splits. The FILLFACTOR value isn’t maintained when inserting new data. It is only effective when we create or rebuild an index. So once it’s full, and between the index rebuilds, the data page will be split again during the next insert.

Things are different with the sequential GUID. Sequential GUIDs are generated in ascending order. The “block” of compact, ever-increasing GUIDs is formed on a server and between the OS restarts. Sequential GUIDs created by the Client code on a different server will fall into a separate “block” of guids – see Figure 6. As mentioned before, sequential GUIDs can be created by SQL Server – NEWSEQUENTIALID() fn. initiated by a DEFAULT constraint and/or by the client code. The compact “blocks” of guids will reduce fragmentation.

Conclusion

In SQL Server, GUID is a 16byte binary value stored as UNIQUIEIDENTIFIER data type. NEWID() and NEWSEQUENTIALID() are the two system functions that can be used to create GUIDs in SQL server. The latter is not compliant with the RFC4122 standard. Both GUID types can be created by the client code using functions: UUidCreate(), UuidCreateSequential(). .NET sorts Guid values differently than SQL Server. UNIQUEIDENTIFIER data type re-shuffles first 8 bytes(the first three segments). .NET’s SqlGuid Struct represents a GUID to be stored or retrieved from a DB.
GUID values are often used as primary key/clustered index key values. The randomness of the GUID values introduces logical and physical data fragmentation, which then leads to query performance regression. Sequential GUIDs can reduce fragmentation but still need to be used carefully and with the cost-benefit approach in mind.

Thanks for reading.

Dean Mincic

Значение NULL

Значение null — это специальное значение, которое можно присвоить ячейке таблицы. Это значение обычно применяется, когда информация в ячейке неизвестна или неприменима. Например, если неизвестен номер домашнего телефона служащего компании, рекомендуется присвоить соответствующей ячейке столбца home_telephone значение null.

Если значение любого операнда любого арифметического выражения равно null, значение результата вычисления этого выражения также будет null. Поэтому в унарных арифметических операциях, если значение выражения A равно null, тогда как +A, так и -A возвращает null. В бинарных выражениях, если значение одного или обоих операндов A и B равно null, тогда результат операции сложения, вычитания, умножения, деления и деления по модулю этих операндов также будет null.

Если выражение содержит операцию сравнения и значение одного или обоих операндов этой операции равно null, результат этой операции также будет null.

Значение null должно отличаться от всех других значений. Для числовых типов данных значение 0 и значение null не являются одинаковыми. То же самое относится и к пустой строке и значению null для символьных типов данных.

Значения null можно сохранять в столбце таблицы только в том случае, если это явно разрешено в определении данного столбца. С другой стороны, значения null не разрешаются для столбца, если в его определении явно указано NOT NULL. Если для столбца с типом данных (за исключением типа TIMESTAMP) не указано явно NULL или NOT NULL, то присваиваются следующие значения:

  • NULL, если значение параметра ANSI_NULL_DFLT_ON инструкции SET равно on.

  • NOT NULL, если значение параметра ANSI_NULL_DFLT_OFF инструкции SET равно on.

Если инструкцию set не активировать, то столбец по умолчанию будет содержать значение NOT NULL. (Для столбцов типа TIMESTAMP значения null не разрешаются.)

Дополнительные соображения

Есть еще несколько вещей, которые стоит принять во внимание. В данной статье мы много времени уделили времени вставки значений в базу, но совершенно упустили из виду время формирования самого GUID, по сравнению с Guid.NewGuid()

Конечно, время формирования дольше. Я могу создать миллион произвольных GUID за 140 ms, но на создание последовательных уйдет 2800 ms, что в 20 раз медленнее.

Быстрые тесты показали, что львиная доля такого замедления приходится на использование сервиса RNGCryptoServiceProvider для генерирования произвольных данных. Переключение на System.Random снизило время выполнения до 400 ms. Я все еще не рекомендую этот способ из-за описанных опасностей.

Является ли такое замедление проблемой? Лично для себя я решил, что нет. До тех пор пока ваше приложение не использует интенсивные вставки данных (а тогда стоит рассмотреть саму целесообразность использования GUID), стоимость генерирования согласуется с временем работы самой базы и ее дальнейшей быстрой работы.

Другая возможная проблема: будут ли 10 байт достаточны для гарантирования уникальности? С учетом временного штампа это означает, что два любых GUID созданных в период больший чем несколько миллисекунд будут гарантированно разными.  Но что с GUID, которые создаются действительно очень быстро в одном промежутке времени. В таком случае 10 байт дают нам оценку в 280, или 1,208,925,819,614,629,174,706,176 возможных комбинаций. Т.е. вероятность будет такой же как и то, что в этот момент ваша база данных и все бэкапы будут одновременно атакованы и уничтожены ордой диких свиней.

Последняя проблема, которая вас может заинтересовать, это то, что полученные GUID технически не совместимы со стандартом RFC 4122. Честно, я не думаю, что это большая проблема, я не знаю ни одной базы данных которая реально проверяет внутреннее устройство GUID и упущение версии GUID дает нам дополнительные байты для увеличения уникальности ключа.

Reason #3. Number of Locks

When statement is being executed, Microsoft SQL Server is locking new data row by row or page by page thinking that the table might be accessed by concurrent query. If you are generating test database, there are no concurrent queries. So we may direct Microsoft SQL Server to lock whole table rather than page by page locking. Table hint locks the whole table at the beginning of execution.

SQL



create table ids(id uniqueidentifier);

declare @ids table(id int identity)
declare @guidCount int = 1000000;
declare @n int = ;
while @n < SQRT(@guidCount)
begin
   insert @ids default values;
   set @n = @n + 1;
end

insert ids with(tablock)
select NEWID() from @ids i1, @ids i2;

This code generates 1 000 000 guids in 500 ms! That’s really a great result. So one can see that this is very important to obtain a table lock before heavy execution. If you want to create even more guids — is the only solution. In my test environment (default Microsoft SQL 2008 settings), I can’t generate more than 10 millions guids without table lock. I get an error:

Msg 1204, Level 19, State 4, Line 10
The instance of the SQL Server Database Engine 
cannot obtain a LOCK resource at this time. 
Rerun your statement when there are fewer active users. 
Ask the database administrator to check the lock and 
memory configuration for this instance, or to check for long-running transactions.

Incremental Primary Keys

As you might guess, the cure for the random primary key anti-pattern is an incremental key pattern. With a uniqueidentifier data type, a sequential value can be assigned by SQL Server using the NEWSEQUENTIALID function (in a default constraint expression) or in application code using the UuidCreateSequential Win32 API call along with some byte swapping (code example below). Alternatively, one can use an integral data type (int, bigint, etc.) along with a value generated by an IDENTITY property or a SEQUENCE object. The advantage of an integral type is the reduced space requirements compared to a 16-byte uniqueidentifier. The advantage of a uniqueidentifier is that it can easily be generated in application code before database persistence without a database round trip, which is desirable for distributed applications and when keys of related tables are assigned in application code before writing to the database.

Figure 2 shows the same test using a sequential key value. Over 2.2M rows were inserted in 15 minutes. As you can see, significant performance improvement is achieved with this trivial application change.

Figure 2: Incremental key insert performance

Listing 1 shows the T-SQL code I used for these performance tests and listing 2 contains the C# code (with the random GUID commented out). I generated the uniqueidentifier value via application code in the tests but performance with NEWID() is comparable to the first test and NEWSEQUENTIALID() is similar to the second test.

Listing 1: T-SQL scripts for test table and stored procedure

CREATE TABLE dbo.TestTable(
	TestKey uniqueidentifier NOT NULL
		CONSTRAINT PK_TestTable PRIMARY KEY CLUSTERED
	,TestData char(8000) NOT NULL
);
GO

CREATE PROC dbo.InsertTestTable
	@TestKey uniqueidentifier
	,@TestData char(8000)
AS
SET NOCOUNT ON;
DECLARE @TotalRows int;

--insert row
INSERT INTO dbo.TestTable (TestKey, TestData) 
	VALUES(@TestKey, @TestData);

--update pmon counter for rowcount
SELECT @TotalRows = rows 
FROM sys.partitions 
WHERE object_id = OBJECT_ID(N'TestTable') AND index_id = 1;
EXEC sys.sp_user_counter1 @TotalRows; --for pmon row count

RETURN @@ERROR;
GO

Listing 2: C# insert test console application

using System;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.InteropServices;

namespace UniqueIdentifierPerformanceTest
{
    class Program
    {
        
        public static extern int UuidCreateSequential(ref Guid guid);

        static string connectionString = @"Data Source=MyServer;Initial Catalog=MyDatabase;Integrated Security=SSPI";
        static int rowsToInsert = 10000000;
        static SqlConnection connection;
        static SqlCommand command;

        static void Main(string[] args)
        {

            int rowsInserted = 0;

            using (connection = new SqlConnection(connectionString))
            {
                using (command = new SqlCommand("dbo.InsertTestTable", connection))
                {
                    command.Connection = connection;
                    command.CommandType = CommandType.StoredProcedure;
                    command.Parameters.Add("@TestKey", SqlDbType.UniqueIdentifier);
                    command.Parameters.Add("@TestData", SqlDbType.Char, 8000);

                    connection.Open();

                    while (rowsInserted < rowsToInsert)
                    {
                        //random guid
                        //command.Parameters.Value = Guid.NewGuid();

                        //sequential guid
                        command.Parameters.Value = NewSequentialGuid();
                        command.Parameters.Value = "Test";
                        command.ExecuteNonQuery();
                        ++rowsInserted;

                        //display progress every 1000 rows
                        if (rowsInserted % 1000 == 0)
                        {
                            Console.WriteLine(string.Format(
                                "{0} of {1} rows inserted"
                                , rowsInserted.ToString("#,##0")
                                , rowsToInsert.ToString("#,##0")));
                        }
                    }
                }

                connection.Close();
            }

        }

        /// 
        /// call UuidCreateSequential and swap bytes for SQL Server format
        /// 
        /// sequential guid for SQL Server
        private static Guid NewSequentialGuid()
        {
            const int S_OK = 0;
            const int RPC_S_UUID_LOCAL_ONLY = 1824;

            Guid oldGuid = Guid.Empty;

            int result = UuidCreateSequential(ref oldGuid);
            if (result != S_OK && result != RPC_S_UUID_LOCAL_ONLY)
            {
                throw new ExternalException("UuidCreateSequential call failed", result);
            }

            byte[] oldGuidBytes = oldGuid.ToByteArray();
            byte[] newGuidBytes = new byte;
            oldGuidBytes.CopyTo(newGuidBytes, 0);

            // swap low timestamp bytes (0-3)
            newGuidBytes = oldGuidBytes;
            newGuidBytes = oldGuidBytes;
            newGuidBytes = oldGuidBytes;
            newGuidBytes = oldGuidBytes;

            // swap middle timestamp bytes (4-5)
            newGuidBytes = oldGuidBytes;
            newGuidBytes = oldGuidBytes;

            // swap high timestamp bytes (6-7)
            newGuidBytes = oldGuidBytes;
            newGuidBytes = oldGuidBytes;

            //remaining 8 bytes are unchanged (8-15) 

            return new Guid(newGuidBytes);

        }

    }
}

4.3 Генерация значений колонок

Различные
возможности позволяют вам генерировать значения колонок: тождеств (Identity), NEWID функция
и тип данных uniaueidentifier.

Использование свойства Identity

Вы можете
использовать свойство Identity (тождество) для
создания колонки, которая содержит сгенерированное системой следующее значение,
определяющее каждую строку, вставленную в таблицу. Тождественная колонка часто
используется в качестве значений первичного ключа.

Когда SQL Server
автоматически предоставляет значение ключа, можно уменьшить затраты на
улучшение производительности. Это облегчает программирование, делает первичный
ключ коротким и уменьшает пользовательские транзакции.

Рассмотрите
следующие рекомендации для использования свойства Identity:

 
Только одна тождественная колонка разрешена в таблице;

 
Она должна использоваться с целочисленными типами данных.;

 
Оно не может обновляться;

 
Вы можете использовать ключевое слово IDENTITYCOL в
месте с именем колонки в запросе. Это позволяет вам ссылаться на колонку в
таблице со свойством Identity, не зная имени
колонки;

 
Не разрешает нулевые значения.

Вы можете
получить информацию о свойстве Identity
несколькими путями:

 
Две системные функции возвращают информацию о тождественной
колонке: IDENT_SEED (возвращает начальное значение) и IDENT_INCR (возвращает значение приращения);

 
Вы можете получить информацию о Identity
колонке, используя глобальную переменную @@identity,
которая определяет значение последней вставленной строки в тождественную
колонку в течение сессии;

 
Функция SCOPE_IDENTITY
возвращает последнее значение, вставленное в колонку в некоторых рамках. Рамки
определяются процедурой, триггером, функцией или batch;

 
Функция IDENTITY_INSERT
возвращает последнее значение, сгенерированное для определённой таблицы в любой
сессии или рамке.

Вы можете
управлять свойством Identity несколькими путями:

 
Вы можете позволить явно вставлять значения в тождественную
колонку таблицы с помощью установки свойства IDENTITY_INSERT в
значение ON. Когда это свойство
установлена, оператор INSERT должен указывать значение колонки.

 
Для проверки возможности изменения текущего тождественного поля
таблицы, вы можете использовать оператор DBCC CHECKIDENT.
Он позволяет вам сравнить текущее тождественное значение с максимальным
значением в тождественной колонке.

Тождественное свойство не обеспечивает уникальности. Для
уникальности создавайте уникальный индекс.

Следующий
пример создаёт таблицу с двумя колонками, StudentID
и Name. Свойство Identity
используется для автоматического увеличения в каждой строке колонки StudentID. Начальное значение устанавливается в 100, а
приращение в 5. Значения для этой колонки будут генерироваться в виде 100, 105,
110, 115 …

CREATE
TABLE
Class

(StudentID INT IDENTITY(100,5) NOT NULL,

Namt varchar (16))

Использование функции NEWID и
уникального идентификатора

Тип данных uniqueidentifier (уникальный
идентификатор) и функция NEWID –
это две возможности, которые используются вместе. Используйте эти возможности,
когда данные сопоставляются из нескольких таблиц в одну большую таблице, когда
нужно обеспечить уникальность всех записей:

 
Тип данных uniqueidentifier хранит
число уникального идентификатора в виде 16-байт бинарной строки. Тип данных использует
для хранения глобальный уникальный идентификатор (GUID);

 
Функция NEWID
создаёт новое число уникального идентификатора, которое хранится в типе данных uniqueidentifier;

 
Тип данных uniqueidentifier не
генерирует автоматически новый ID для вставляемой
строки в отличие от свойства Identity.

Пример

Тип данных TIMESTAMP

Тип данных TIMESTAMP указывает столбец, определяемый как VARBINARY(8) или BINARY(8) , в зависимости от свойства столбца принимать значения null. Для каждой базы данных система содержит счетчик, значение которого увеличивается всякий раз, когда вставляется или обновляется любая строка, содержащая ячейку типа TIMESTAMP, и присваивает этой ячейке данное значение. Таким образом, с помощью ячеек типа TIMESTAMP можно определить относительное время последнего изменения соответствующих строк таблицы. (ROWVERSION является синонимом TIMESTAMP.)

Само по себе значение, сохраняемое в столбце типа TIMESTAMP, не представляет никакой важности. Этот столбец обычно используется для определения, изменилась ли определенная строка таблицы со времени последнего обращения к ней

Unique Identifier – A Globally Unique ID (GUID)

The uniqueidentifier type is used when you which to store a GUID (Globally Unique ID).  The main purpose to create GUID is create an ID that is unique across many computers within a network.

Unique Identifiers take the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx where x represents a hexadecimal value (e.g. 0-F).

An example of an GUID is F22620D0-600E-4F0D-86E3-71250D1CE01E.

You can use the NEWID() function to generate GUID’s.

Here is an example:

Which when I ran it returned 0AFEBE69-7B1E-43F9-909E-35E7E32535B2.  When you run it, it will create a different GUID as you’re running it on a different computer at another time.

Unique Identifiers are important to SQL as they’re used when replicating data.

Рейтинг
( Пока оценок нет )
Editor
Editor/ автор статьи

Давно интересуюсь темой. Мне нравится писать о том, в чём разбираюсь.

Понравилась статья? Поделиться с друзьями:
Вадлейд
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: