SQL Server 2016 CTP3.1 — что нового для разработчика?

  • Tutorial
Еще недавно в моей памяти отложился анонс SQL Server 2016, которую лично презентовал Сатья Наделла. И вдруг, как снег на голову, стали одна за одной выходить свежие Community Technology Preview (на данный момент самая свежая версия – CTP3.1). По мере знакомства с новой версией, все больше хотелось поделиться впечатлениями…

Далее обзор новых синтаксических фишек SQL Server 2016: JSON, GZIP, DROP IF EXISTS, TRUNCATE TABLE по секциям, новые функции…

#1 – DROP IF EXISTS


CREATE TABLE dbo.tbl (
    a INT, b INT,
    CONSTRAINT ck CHECK (a > 1),
    INDEX ix CLUSTERED (a)
)

Если раньше перед удалением объекта приходилось делать проверку:

IF OBJECT_ID(N'dbo.tbl', 'U') IS NOT NULL
    DROP TABLE dbo.tbl

То сейчас появился более компактный синтаксис:

DROP TABLE IF EXISTS dbo.tbl

Добавлена возможность удаление дочерних элементов:

ALTER TABLE dbo.tbl DROP COLUMN IF EXISTS b
ALTER TABLE dbo.tbl DROP CONSTRAINT IF EXISTS ck
ALTER TABLE dbo.tbl DROP CONSTRAINT IF EXISTS ix

Для одного родительского объекта конструкции можно объединять:

ALTER TABLE dbo.tbl DROP 
    COLUMN IF EXISTS b,
    CONSTRAINT IF EXISTS ck,
    CONSTRAINT IF EXISTS ix

Поддерживается DROP IF EXISTS практически для всех объектов (с полным списком можно ознакомиться тут):

DROP TABLE IF EXISTS #temp
DROP TABLE IF EXISTS ##temp
DROP VIEW IF EXISTS dbo.view1
DROP PROCEDURE IF EXISTS dbo.proc1
DROP DATABASE IF EXISTS db

#2 – SESSION_CONTEXT


В моей практике возникали задачи по расшариванию параметров в рамках пользовательской сессии. Раньше для этого приходилось использовать CONTEXT_INFO размер которой был ограничен 128 байтами:

DECLARE
      @UserID SMALLINT = 1
    , @LocaleID INT = 123

DECLARE @ctn VARBINARY(128)
SET @ctn = CAST(@UserID AS BINARY(2)) + CAST(@LocaleID AS BINARY(4))
SET CONTEXT_INFO @ctn

Теперь все стало чуточку удобнее за счет новой функции SESSION_CONTEXT в которой разрешили хранить 256Кб на сессию:

EXEC sys.sp_set_session_context @key = N'UserID', @value = 1
EXEC sys.sp_set_session_context @key = N'LocaleID', @value = 123

SELECT
      UserID = SESSION_CONTEXT(N'UserID')
    , LocaleID = SESSION_CONTEXT(N'LocaleID')

#3 – CHECKDB + MAXDOP


По умолчанию, при выполнении DBCC CHECKDB используется количество потоков равное числу логических ядер. Теперь количество потоков можно ограничить, чтобы не снижать производительность сервера в целом:

DBCC CHECKDB(N'AdventureWorks2016CTP3') WITH MAXDOP = 4

Аналогичная функциональность добавлена в DBCC CHECKTABLE и DBCC CHECKFILEGROUP:

USE AdventureWorks2016CTP3
GO

DBCC CHECKTABLE('HumanResources.Employee') WITH MAXDOP = 4
DBCC CHECKFILEGROUP(1) WITH MAXDOP = 4

Для DBCC CHECKDB сделали некоторые оптимизации при проверке фильтрованных индексов и COMPUTED столбцов с опцией PERSISTED. Еще сократили время проверки для таблиц, которые содержат большое количество секций.

#4 – FORMATMESSAGE


В предыдущих версия функция FORMATMESSAGE могла использовать только ранее добавленные пользовательские сообщения:

EXEC sys.sp_addmessage
    @msgnum = 66667,
    @severity = 16,
    @msgtext = N'param1: %s, param2: %s'

DECLARE @msg NVARCHAR(2048) = FORMATMESSAGE(66667, N'one', N'two')
SELECT @msg

теперь появилась возможность указывать произвольную маску:

SELECT FORMATMESSAGE('val1: %+i, val2: %+d', 5, -6)

Многие рутинные операции квотирования или конкатенации строк можно сделать элегантнее:

SELECT FORMATMESSAGE('SELECT * FROM [%s].[%s]', SCHEMA_NAME([schema_id]), name)
FROM sys.objects
WHERE [type] = 'U'

#5 – COMPRESS & DECOMPRESS


В новой редакции появилась встроенная поддержка GZIP: COMPRESS и DECOMPRESS. При декодировании важно следить за правильным типов данных в который преобразуется результат:

DECLARE @a VARBINARY(MAX) = COMPRESS('test test test')

SELECT
      @a
    , DECOMPRESS(@a)
    , CAST(DECOMPRESS(@a) AS NVARCHAR(MAX)) -- печаль
    , CAST(DECOMPRESS(@a) AS VARCHAR(MAX))

Кодируем ANSI строку и пытаемся декодировать полученное значение:

------------------------------------ ---------------------- ---------------- -----------------
0x1F8B08000000000004002B492D2E512    0x746573742074657374   整瑳琠獥⁴整瑳     test test test

#6 – DATEDIFF_BIG


В SQL Server 2008 для функции DATEDIFF появились новые параметры: MICROSECOND и NANOSECOND, но когда задавался слишком большой диапазон дат:

SELECT DATEDIFF(NANOSECOND, '20000101', '20160101') 

это могло привести к ошибке:

Msg 535, Level 16, State 0, Line 1
The datediff function resulted in an overflow. The number of dateparts separating two date/time instances is too large.
Try to use datediff with a less precise datepart.

Для таких ситуаций добавили новую функцию DATEDIFF_BIG:

SELECT DATEDIFF_BIG(NANOSECOND, '20000101', '20160101') 

#7 – AT TIME ZONE


В CTP3.0 появилось новое системное представление:

SELECT name, current_utc_offset, is_currently_dst
FROM sys.time_zone_info

в котором можно получить список часовых зон:

name                       current_utc_offset is_currently_dst
-------------------------- ------------------ ----------------
Dateline Standard Time     -12:00             0
UTC-11                     -11:00             0
...
Central Standard Time      -06:00             0
...
Pacific SA Standard Time   -03:00             0
UTC-02                     -02:00             0
...
UTC                        +00:00             0
GMT Standard Time          +00:00             0
Greenwich Standard Time    +00:00             0
...
Belarus Standard Time      +03:00             0
Russian Standard Time      +03:00             0
...

С помощью AT TIME ZONE можно выводить время в заданной часовом поясе:

SELECT CONVERT(DATETIME2, GETDATE()) AT TIME ZONE N'Belarus Standard Time'

----------------------------------
2015-12-02 14:51:02.1366667 +03:00

который можно параметризировать:

DECLARE @tz NVARCHAR(256) = N'Belarus Standard Time'
SELECT
      GETDATE() AT TIME ZONE @tz
    , CONVERT(DATETIME2, GETDATE()) AT TIME ZONE @tz

----------------------------------
2015-12-02 14:51:28.6266667 +03:00

В чем польза от такого нововведения? Например, можно вывести сколько времени в других часовых поясах исходя из текущего времени у нас:

SELECT name, CONVERT(DATETIME, 
    SWITCHOFFSET(SYSUTCDATETIME() AT TIME ZONE name,
    DATENAME(TzOffset, SYSDATETIMEOFFSET()))
)
FROM sys.time_zone_info

---------------------------------- -----------------------
Dateline Standard Time              2015-12-03 02:56:41.940
UTC-11                              2015-12-03 01:56:41.940
...
Pacific SA Standard Time            2015-12-02 17:56:41.940
UTC-02                              2015-12-02 16:56:41.940
...
UTC                                 2015-12-02 14:56:41.940
GMT Standard Time                   2015-12-02 14:56:41.940
Greenwich Standard Time             2015-12-02 14:56:41.940
Central European Standard Time      2015-12-02 13:56:41.940
...

#8 – JSON


Поддержка JSON – это одна из основных фишек SQL Server 2016. Начиная с CTP2.0 появилась возможность генерировать JSON по аналогии с XML. Поддерживаются две конструкции FOR JSON AUTO и FOR JSON PATH:

SELECT TOP (2) name, database_id, source_database_id, create_date 
FROM sys.databases
FOR JSON AUTO, ROOT('root')

{"root":
    [
        {"name":"master","database_id":1,"create_date":"2003-04-08T09:13:36.390"},
        {"name":"tempdb","database_id":2,"create_date":"2015-12-02T11:34:36.080"}
    ]
}

SELECT TOP (2)
      name
    , [db.id] = database_id
    , [db.scr_id] = source_database_id
    , [db.date] = create_date 
FROM sys.databases
FOR JSON PATH, ROOT

{"root":
    [
        {
            "name":"master",
            "db":{"id":1,"date":"2003-04-08T09:13:36.390"}
        },
        {
            "name":"tempdb",
            "db":{"id":2,"date":"2015-12-02T11:34:36.080"}
        }
    ]
}

Чтобы NULL значения при генерации включались в JSON нужно использовать опцию INCLUDE_NULL_VALUES:

SELECT TOP (1) name, database_id, source_database_id
FROM sys.databases
FOR JSON AUTO, INCLUDE_NULL_VALUES

[
    {
        "name":"master",
        "database_id":1,
        "source_database_id":null
    }
]

Если нужно удалить квадратные скобки из JSON, то для этого предусмотрена опция WITHOUT_ARRAY_WRAPPER (которая доступна начиная с версии CTP3.2):

SELECT TOP (1) name, database_id, source_database_id
FROM sys.databases
FOR JSON AUTO, WITHOUT_ARRAY_WRAPPER

{
    "name":"master",
    "database_id":1
}

Для хранения JSON необходимо использовать NVARCHAR, поскольку отдельного типа данных не предусмотрено:

DECLARE @json NVARCHAR(MAX) = (
        SELECT key1 = 1, key2 = GETDATE()
        FOR JSON PATH
    )

SELECT @json

{"key1":1,"key2":"2015-12-02T15:45:05.530"}

Чтобы сделать выборку из JSON можно использовать OPENJSON. Если запись одна, то возвращается результат в виде «key-value»:

DECLARE @json NVARCHAR(MAX) = N'
    {
        "UserID" : 1,
        "UserName": "JC Denton",
        "IsActive": true,
        "RegDate": "2015-12-02"
    }';

SELECT * FROM OPENJSON(@json)

key         value        type
----------- ------------ ----
UserID      1            2
UserName    JC Denton    1
IsActive    true         3
RegDate     2015-12-02   1

Такое поведение можно использовать в качестве «еще одного варианта» сплита строки:

DECLARE @a NVARCHAR(100) = '1,2,3'

SELECT CAST(value AS INT)
FROM OPENJSON(N'[' + @a + N']')

-----------
1
2
3

Если записей несколько:

DECLARE @json NVARCHAR(MAX) = N'
[
    {
        "UserID" : 1,
        "UserName": "JC Denton",
        "IsActive": true,
        "RegDate": "2015-12-02"
    },
    {
        "UserID" : 2,
        "UserName": "Paul Denton",
        "IsActive": false,
        "RegDate": "2015-11-02"
    }
]';

SELECT * FROM OPENJSON(@json)

то результат будет следующим:



В нормальном виде данные можно будет получить так:

DECLARE @json NVARCHAR(MAX) = N'
[
    {
        "UserID" : 1,
        "UserName": "JC Denton",
        "IsActive": true,
        "RegDate": "2015-12-02"
    },
    {
        "UserID" : 2,
        "UserName": "Paul Denton",
        "IsActive": 0,
        "RegDate": "2015-11-02"
    }
]';

SELECT *
FROM OPENJSON(@json)
    WITH
    (
        UserID INT, 
        UserName VARCHAR(50),
        IsActive BIT,
        [Date] DATE '$.RegDate'
    )

UserID      UserName        IsActive Date
----------- --------------- -------- ----------
1           JC Denton       1        2015-12-02
2           Paul Denton     0        2015-11-02

Если нужно получить скалярное выражение, то можно использовать JSON_VALUE:

DECLARE @json NVARCHAR(4000) = N'
{
    "UserID" : 1,
    "Detail": [ 
        { "Year":2016 },
        { "Year":2015, "Options": [{ "Visible":true }]
    ]
}'

SELECT 
      JSON_VALUE(@json, '$.UserID')
    , JSON_VALUE(@json, '$.Detail[0].Year')
    , JSON_VALUE(@json, '$.Detail[1].Year')
    , JSON_VALUE(@json, '$.Detail[1].Options[0].Visible')

Для получения массива объектов из JSON предусмотрена функция JSON_QUERY:

DECLARE @json NVARCHAR(4000) = N'
{
    "Main" :{  
      "Detail": {  
        "Name":"color",
        "Value":"blue"
      }
}'

SELECT JSON_QUERY(@json, '$.Main.Detail')

Можно использовать функцию ISJSON, если нужно удостовериться что текст является JSON:

DECLARE @json NVARCHAR(MAX) = N'{"ID" : 1}';
SELECT ISJSON(@json), ISJSON('')

Специальных индексов для JSON не предусмотрено, но существует возможность использовать COMPUTED столбцы:

DROP TABLE IF EXISTS dbo.Users
CREATE TABLE dbo.Users (
    OrderID INT PRIMARY KEY,
    JSON NVARCHAR(4000),
    CONSTRAINT CK_IsJSON CHECK (ISJSON(JSON)=1),
    Age AS (CONVERT(INT, JSON_VALUE(JSON, '$.Age')))
)
CREATE INDEX IX_Age ON dbo.Users(Age)

Сейчас не для всех COMPUTED столбцов на основе JSON можно создать индекс:

ALTER TABLE dbo.Users
    ADD RegDate AS (CAST(JSON_VALUE(JSON, '$.Age') AS DATE)) 
GO
CREATE INDEX IX_RegDate ON dbo.Users(RegDate)

Msg 2729, Level 16, State 1, Line 15
Column 'RegDate' in table 'dbo.Users' cannot be used in an index or statistics or as a partition key because it is non-deterministic.

этот баг должны исправить в следующей версии CTP.

#9 – ONLINE ALTER COLUMN


Команду ALTER COLUMN теперь можно проводить в режиме ONLINE. При выполнении команды данные по столбцу будут доступны для чтения, а блокировка схемы Sch-M накладывается лишь в самом конце операции ALTER, когда происходит переключение на новые страницы с данными (более детально можно прочитать тут).

DROP TABLE IF EXISTS dbo.tbl
CREATE TABLE dbo.tbl (x VARCHAR(255) NULL)
GO

ALTER TABLE dbo.tbl
    ALTER COLUMN x VARCHAR(255) NOT NULL
    WITH (ONLINE = ON)
GO

ALTER TABLE dbo.tbl
ALTER COLUMN x NVARCHAR(255)
    COLLATE Cyrillic_General_100_CI_AS NOT NULL
    WITH (ONLINE = ON)

#10 – TRUNCATE TABLE + PARTITIONS


В версии CTP2.0 для операции TRUNCATE TABLE добавили возможность работы с отдельными секциями, а не только над всей таблицей. При этом можно указывать не только отдельную секцию, но целый диапазон секций. Создадим тестовую таблицу:

CREATE PARTITION FUNCTION PF (SMALLINT) AS RANGE RIGHT FOR VALUES (1, 2, 3, 4, 5)
GO
CREATE PARTITION SCHEME PS AS PARTITION PF ALL TO ([PRIMARY])
GO
DROP TABLE IF EXISTS dbo.tbl
CREATE TABLE dbo.tbl (a SMALLINT PRIMARY KEY) ON PS (a)
GO

INSERT INTO dbo.tbl (a)
VALUES (0), (1), (2), (3), (4), (5)

SELECT partition_number, [rows]
FROM sys.partitions
WHERE [object_id] = OBJECT_ID('dbo.tbl')
    AND index_id < 2

---------------- ------
1                1
2                1
3                1
4                1
5                1
6                1

TRUNCATE TABLE dbo.tbl WITH (PARTITIONS (1, 4 TO 5))

partition_number rows
---------------- ------
1                0
2                1
3                1
4                0
5                0
6                1

#11 – CURRENT_TRANSACTION_ID


В CTP3.0 еще добавилась новая функция CURRENT_TRANSACTION_ID, которая исходя из названия возвращает текущую транзакцию… Возможно, это функция и будет полезной кому-то, но для нее более наглядного примера мне не удалось придумать:

BEGIN TRANSACTION
    UPDATE HumanResources.Employee
    SET SalariedFlag = 0 
    WHERE BusinessEntityID = 1

SELECT ec.session_id, s.[text]
FROM sys.dm_tran_session_transactions t
JOIN sys.dm_exec_connections ec ON t.session_id = ec.session_id
CROSS APPLY sys.dm_exec_sql_text(ec.most_recent_sql_handle) s
WHERE t.transaction_id != CURRENT_TRANSACTION_ID()

session_id  text
----------- ---------------------------
67          BEGIN TRANSACTION 
            UPDATE HumanResources.Employee
            SET SalariedFlag = 0 
            WHERE BusinessEntityID = 1

Небольшое послесловие...

Из того, что я сейчас наблюдаю, релиз SQL Server 2016 обещает быть очень интересным. С каждым новым CTP добавляется большое количество фишек, которые сложно описать в рамках одной статьи. Чтобы сохранить читабельность, за бортом этого обзора я оставил Temporal Tables, Dynamic Data Masking и улучшения в In-Memory, которые планирую добавить в скором продолжении.

Если хотите поделиться этой статьей с англоязычной аудиторией:
SQL Server 2016 CTP3.1 – What’s New for Developer?
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 13

    0
    А что там за версия Visual Studio?
      +1
      Если Вы имели ввиду поддержку SQL Server 2016 в VS, то SSDT регулярно обновляется.

      Если речь идет про SSMS, то начиная с 2016 версии… SSMS развивается независимо от SQL Server.

      Если вопрос был про шелл SSMS, то его тоже обещают обновить до VS 2015. Сейчас шелл от VS 2010.
        0
        Да, вопрос был про SSMS.
      0
      DROP IF EXISTS
      А я всегда просто игнорировал возможную ошибку при выполнении DROP. Чем это хуже?
        +1
        BEGIN TRY
            BEGIN TRANSACTION
        
            --IF OBJECT_ID('tempdb.dbo.##temp') IS NOT NULL
            --	DROP TABLE ##temp
            
            SELECT *
            INTO ##temp
            FROM sys.objects
        
            SELECT *
            FROM ##temp
        
            COMMIT TRANSACTION
        END TRY
        BEGIN CATCH
        
            --PRINT ERROR_MESSAGE()
        
            IF XACT_STATE() <> 0
                ROLLBACK TRANSACTION
        
        END CATCH
        

        хоть пример и натянутый… но все же… ошибки игнорировать не комильфо. А чтобы ошибок не было… нужно писать код с учетом проверок к которым DROP IF EXISTS и относится.
          +1
          Разве ошибка при выполнении DROP может быть только по поводу отсутствующего объекта? Вы все ошибки при выполнении DROP игнорировали?
          +1
          Поддержка JSON — это очень круто, как мне кажется. Теперь же есть возможность отказаться от прослойки в виде, например веб-сервера, для общения SQL сервера с внешним миром в этом формате.
            +2
            с внешним миром
            Это с кем — с веб-клиентами что ли? :)
            Но с Node.js будет удобнее работать, да.
              0
              Как у Oracle? Если кратко: в свое время была разработана технология, позволяющая минимально использовать посредников между базой данных и браузером. Не особо взлетело, однако идея интересная.

              А по факту: добавление JSON является очередным давлением на конкурентов: как на хранилища без схемы, так и на PosrgeSQL, который всё это уже умеет.
                +1
                Я бы сказал, что наоборот, добавление JSON является результатом давления конкурентов на SQL Server. :)
                +3
                Там, насколько я понял по NVarChar — тупо преобразование в JSON и обратно. О таком масштабе поддержки как в Postgre — с BSON, индексами, и всяким таким — речь не идет.
                  +1
                  Вы, конечно, правы. В текущей реализации нельзя говорить о прямой конкуренции с JSON функционалом реализованным в PostgreSQL.

                  Хотя мне кажется данная ситуация повторит судьбу с Columnstore Indexes. В них была куча ограничений, когда они появились в 2012 версии. В 2014 часть этих ограничений сняли (например, теперь индексы стали обновляемыми) и повысили производительность при их использовании. В 2016 версии для Columnstore Indexes еще больше фишек добавили… Надеюсь тоже ждет и JSON на SQL Server :)
                0
                Шардинга мы так и не увидим, походу, никогда

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое