Devart Blog

SQL Server 2016 CTP3.1 – What’s New for Developer?

Posted by on December 9th, 2015

Not that long ago, I remember Microsoft’s CEO Satya Nadella has been announcing a new version of SQL Server 2016. And all of a sudden fresh Community Technology Preview versions started appearing one by one (currently the latest version is CTP3.1). Having tested the new version, I would like to share my impressions.

I will now review the new T-SQL syntax features of SQL Server 2016: JSON, GZIP, DROP IF EXISTS, TRUNCATE TABLE by partitions…

# 1 – DROP IF EXISTS

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

Earlier, I had to check before deleting an object:

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

But now, there is a shorter syntax:

DROP TABLE IF EXISTS dbo.tbl

In the easy way, you can delete child objects:

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

DROP statements can be combined for one parent object:

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

DROP IF EXISTS is supported for almost all user objects (the complete list can be found here):

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

In my practice, there were tasks on sharing parameters within a user session. Previously, I had to use CONTEXT_INFO, which was limited in size to 128 bytes:

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

Now, everything has become a bit more comfortable with the new SESSION_CONTEXT function, which allows to store 256KB per session:

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

By default, upon execution of DBCC CHECKDB command, the number of threads equal to the number of logical cores was used. Now, you can limit the number of threads so as not to reduce server performance on the whole:

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

Similar functionality is added to DBCC CHECKTABLE and DBCC CHECKFILEGROUP:

USE AdventureWorks2016CTP3
GO

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

DBCC CHECKDB was slightly optimized in terms of checking filtered indexes and the PERSISTED COMPUTED columns. Additionally, the time for checking tables containing a large number of partitions was reduced.

# 4 – FORMATMESSAGE

In the previous versions, the FORMATMESSAGE function could use only previously added custom messages:

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

It is now possible to specify a random mask:

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

Many routine operations (like quoting or string concatenation) can be done more nicely:

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

# 5 – COMPRESS & DECOMPRESS

The integrated support for GZIP: COMPRESS and DECOMPRESS appeared in the new edition. In decoding, it is important to check the correct datatype to which the output is converted:

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

SELECT
      @a
    , DECOMPRESS(@a)
    , CAST(DECOMPRESS(@a) AS NVARCHAR(MAX)) -- issue
    , CAST(DECOMPRESS(@a) AS VARCHAR(MAX))

Here, I encode the ANSI string and try to decode the received value:

----------------------------------------------------------- -------------------------------- ---------------- -----------------
0x1F8B08000000000004002B492D2E5128811100026A5B230E000000    0x7465737420746573742074657374   整瑳琠獥⁴整瑳     test test test

# 6 – DATEDIFF_BIG

In SQL Server 2008, new parameters, MICROSECOND and NANOSECOND, were added to the DATEDIFF function, but when an excessively large date range was specified:

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

this could lead to an error:

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.

The new DATEDIFF_BIG feature was added for such situations:

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

# 7 – AT TIME ZONE

In CTP3.0, a new system presentation appeared:

SELECT name, current_utc_offset, is_currently_dst
FROM sys.time_zone_info

where you can get a list of time zones:

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
...

Using AT TIME ZONE, you can display the time in a given time zone:

SELECT CONVERT(DATETIME2, GETDATE()) AT TIME ZONE N'Belarus Standard Time'
----------------------------------
2015-12-02 14:51:02.1366667 +03:00

which can be parameterized:

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

What are the benefits of this feature? For example, you can display the time in other time zones based on the local time zone:

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

Support for JSON is one of the major features of SQL Server 2016. Starting from CTP2.0, you can generate JSON, by analogy with XML. The two constructions FOR JSON AUTO and FOR JSON PATH are supported:

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"}
        }
    ]
}

For the NULL values to be included in JSON upon generation, use INCLUDE_NULL_VALUES option:

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
    }
]

NVARCHAR should be used for storing JSON, since no particular data type is provided:

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

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

You can use OPENJSON to make a selection from JSON. If there is only one record, then the result will be a «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

This behavior can be used as “one more variant” of the string split:

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

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

In case of several records:

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)

the result is as follows:

Parse data from JSON is quite simple:

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

Use JSON_VALUE if you want to get a scalar expression:

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')

You can use ISJSON, if you want to make sure that the text is JSON:

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

For extracts an object or an array from a JSON string use JSON_QUERY:

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

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

Also need keep in mind, that no specific type of indexes are provided for JSON, but there is a possibility to use COMPUTED columns:

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)

Now, an index can be created not for all JSON-based COMPUTED columns:

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.

This bug should be fixed in the next version of CTP.

# 9 – ONLINE ALTER COLUMN

The ALTER COLUMN command can now be executed in the ONLINE mode. When you execute ALTER COLUMN, the column data will be available for reading, and blocking the Sch-M schema is applied only at the end of the ALTER operation, when switching to new pages with data (more details can be found here)

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

In CTP2.0, the ability to work with separate partitions, not only with the entire table, was added for the TRUNCATE TABLE operation. You can specify not only a separate partition, but the whole range. Let’s create a table with partitions:

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
partition_number rows
---------------- ------
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

A small postscript…

From what I see now, the new version of SQL Server 2016 promises to be very interesting. Each new CTP adds a large number of features, which are difficult to describe in one article. To preserve readability, I left behind Temporal Tables, Dynamic Data Masking and improvements in In-Memory, which I plan to discuss soon.

One Response to “SQL Server 2016 CTP3.1 – What’s New for Developer?”

  1. Leslie Cecile Brown Says:

    Great article! I am working on an executive overview that I need to present to my manager, can anybody point me to some other high-level discussions on this topic, I’m not a great writer (except when it comes to code :)) and I would like to see how some people describe it in a way that’s easy for non-technical folks … Thanks

Leave a Reply