Intro
In this article we will see how you can combine NoSQL and relational concepts in SQL Server database.
We will see how you can transform tables to JSON objects and vice versa using FOR JSON and OPENJSON functions.
Description
Traditional relational schema design follow strict normalization approach – every logical entity is stored as a separate table and there are foreign key relationships between related entities. Imagine products that may have many reviews – you should create one table for products, another for reviews, propagate foreign key from product into reviews table, and add a foreign key relationship between them.
Although this is a proper database design, you might end-up with a lot of tables and foreign key relationships, you will need to join tables every time you need to take product information, maintain indexes o foreign key columns to speed-up joins, etc. Example of query that read products and related reviews is shown in following example:
SELECT Production.Product.ProductID AS ID, Production.Product.Name, Color, ListPrice,
ReviewerName, ReviewDate, Rating
FROM Production.Product
JOIN Production.ProductReview
ON Production.ProductReview.ProductID = Production.Product.ProductID
Every time you need to retrieve related reviews you need to join tables and you would probably need to create additional index on foreign key column in Review table. Results are shown in the following table:
ID | Name | Color | ListPrice | ReviewerName | ReviewDate | Rating |
709 | Mountain Bike Socks, M | White | 9.50 | John Smith | 2007-10-20 00:00:00.000 | 5 |
937 | HL Mountain Pedal | Silver/Black | 80.99 | David | 2007-12-15 00:00:00.000 | 4 |
937 | HL Mountain Pedal | Silver/Black | 80.99 | Jill | 2007-12-17 00:00:00.000 | 2 |
798 | Road-550-W Yellow, 40 | Yellow | 1120.49 | Laura Norman | 2007-12-17 00:00:00.000 | 5 |
On the client-side you probably need to process results and merge two 937 product rows as a single object with two related reviews items (e.g. as an array with two elements). Alternative approach would be not to use this query at all and execute two separate queries where one read product information, while other reads related reviews (probably you would need to use MARS connection in this case). This is not so bad unless if you have a lot of related entities (e.g. product images, product attachments, product categories, etc.) and you need to run several independent queries for a single product.
One of the reasons why NoSQL systems become popular is the fact that you can store composite objects where you can store attributes of primary entity (product in our example) with related records (product reviews) within the primary entity as an array or collection. As an example in MongoDb, DocumentDb you will create one JSON document for Product and add related reviews as an array of JSON objects. You have simpler data model, no JOINs, no additional requests/queries/indexes, all data available in the same record. However, this model is also far from perfect. Although it is good choice for smaller data models, in more complex models you might end-up with heavy objects, or you would need to organize objects in separate collections. In some cases you might need to join objects stored in different collection, and you would find that this is not possible or you need to write complex map/reduce jobs for a simple join.
SQL Server 2016/Azure SQL Database introduce hybrid approach where you can choose between relational model and NoSQL concepts. As an example, if you have products and their reviews you don’t need to create separate tables if you don’t want them. You can create additional columns in the primary table that will contain collection of related entities formatted as JSON arrays. If you already have separate tables and you want to simplify your design, you can add a single column where you can store related reviews:
ALTER TABLE Production.Product
ADD Reviews NVARCHAR(MAX)
CONSTRAINT [Reviews are formatted as JSON] CHECK(ISJSON(Reviews)>0)
In this example, we are adding simple text column with constraint that specifies that reviews should be formatted as JSON (similar to NoSQL databases). There is no new syntax for JSON constraint - you can use standard check constraint with function ISJSON that checks is Reviews text formatted as a JSON object.
If we want to move related product reviews from a separate table into this column we can use a simple UPDATE query:
UPDATE Production.Product
SET Reviews = (
SELECT ReviewerName AS [Reviewer.Name],
EmailAddress AS [Reviewer.E-mail],
ReviewDate, Rating, Comments AS Comment, ModifiedDate
FROM Production.ProductReview
WHERE Production.ProductReview.ProductID = Production.Product.ProductID
FOR JSON PATH)
Inner query fetches all related reviews, formats them as JSON documents using FOR JSON clause and stores them as JSON text in Reviews column. We can format properties in JSON document using dot syntax (e.g. Product.Name will be created as a Name property in Product object).
Now, we can read products and related reviews with a single query:
SELECT ProductID, Name, Color, ListPrice, Reviews
FROM Production.Product
WHERE Reviews IS NOT NULL
Results are shown in the following table:
ProductID | Name | Color | ListPrice | Reviews |
709 | Mountain Bike Socks, M | White | 9.5 | [{"Reviewer":{"Name":"John Smith","E-mail":"john@fourthcoffee.com"},"ReviewDate":"2007-10-20T00:00:00","Rating":5,"Comment":"I can't believe I'm singing the praises of a pair of socks, but I just came back from a grueling\r\n3-day ride and these socks really helped make the trip a blast. They're lightweight yet really cushioned my feet all day. \r\nThe reinforced toe is nearly bullet-proof and I didn't experience any problems with rubbing or blisters like I have with\r\nother brands. I know it sounds silly, but it's always the little stuff (like comfortable feet) that makes or breaks a long trip.\r\nI won't go on another trip without them!","ModifiedDate":"2007-10-20T00:00:00"}] |
798 | Road-550-W Yellow, 40 | Yellow | 1120.49 | [{"Reviewer":{"Name":"Laura Norman","E-mail":"laura@treyresearch.net"},"ReviewDate":"2007-12-17T00:00:00","Rating":5,"Comment":"The Road-550-W from Adventure Works Cycles is everything it's advertised to be. Finally, a quality bike that\r\nis actually built for a woman and provides control and comfort in one neat package. The top tube is shorter, the suspension is weight-tuned and there's a much shorter reach to the brake\r\nlevers. All this adds up to a great mountain bike that is sure to accommodate any woman's anatomy. In addition to getting the size right, the saddle is incredibly comfortable. \r\nAttention to detail is apparent in every aspect from the frame finish to the careful design of each component. Each component is a solid performer without any fluff. \r\nThe designers clearly did their homework and thought about size, weight, and funtionality throughout. And at less than 19 pounds, the bike is manageable for even the most petite cyclist.\r\n\r\nWe had 5 riders take the bike out for a spin and really put it to the test. The results were consistent and very positive. Our testers loved the manuverability \r\nand control they had with the redesigned frame on the 550-W. A definite improvement over the 2002 design. Four out of five testers listed quick handling\r\nand responsivness were the key elements they noticed. Technical climbing and on the flats, the bike just cruises through the rough. Tight corners and obstacles were handled effortlessly. The fifth tester was more impressed with the smooth ride. The heavy-duty shocks absorbed even the worst bumps and provided a soft ride on all but the \r\nnastiest trails and biggest drops. The shifting was rated superb and typical of what we've come to expect from Adventure Works Cycles. On descents, the bike handled flawlessly and tracked very well. The bike is well balanced front-to-rear and frame flex was minimal. In particular, the testers\r\nnoted that the brake system had a unique combination of power and modulation. While some brake setups can be overly touchy, these brakes had a good\r\namount of power, but also a good feel that allows you to apply as little or as much braking power as is needed. Second is their short break-in period. We found that they tend to break-in well before\r\nthe end of the first ride; while others take two to three rides (or more) to come to full power. \r\n\r\nOn the negative side, the pedals were not quite up to our tester's standards. \r\nJust for fun, we experimented with routine maintenance tasks. Overall we found most operations to be straight forward and easy to complete. The only exception was replacing the front wheel. The maintenance manual that comes\r\nwith the bike say to install the front wheel with the axle quick release or bolt, then compress the fork a few times before fastening and tightening the two quick-release mechanisms on the bottom of the dropouts. This is to seat the axle in the dropouts, and if you do not\r\ndo this, the axle will become seated after you tightened the two bottom quick releases, which will then become loose. It's better to test the tightness carefully or you may notice that the two bottom quick releases have come loose enough to fall completely open. And that's something you don't want to experience\r\nwhile out on the road! \r\n\r\nThe Road-550-W frame is available in a variety of sizes and colors and has the same durable, high-quality aluminum that AWC is known for. At a MSRP of just under $1125.00, it's comparable in price to its closest competitors and\r\nwe think that after a test drive you'l find the quality and performance above and beyond . You'll have a grin on your face and be itching to get out on the road for more. While designed for serious road racing, the Road-550-W would be an excellent choice for just about any terrain and \r\nany level of experience. It's a huge step in the right direction for female cyclists and well worth your consideration and hard-earned money.","ModifiedDate":"2007-12-17T00:00:00"}] |
937 | HL Mountain Pedal | Silver/Black | 80.99 | [{"Reviewer":{"Name":"David","E-mail":"david@graphicdesigninstitute.com"},"ReviewDate":"2007-12-15T00:00:00","Rating":4,"Comment":"A little on the heavy side, but overall the entry\/exit is easy in all conditions. I've used these pedals for \r\nmore than 3 years and I've never had a problem. Cleanup is easy. Mud and sand don't get trapped. I would like \r\nthem even better if there was a weight reduction. Maybe in the next design. Still, I would recommend them to a friend.","ModifiedDate":"2007-12-15T00:00:00"},{"Reviewer":{"Name":"Jill","E-mail":"jill@margiestravel.com"},"ReviewDate":"2007-12-17T00:00:00","Rating":2,"Comment":"Maybe it's just because I'm new to mountain biking, but I had a terrible time getting use\r\nto these pedals. In my first outing, I wiped out trying to release my foot. Any suggestions on\r\nways I can adjust the pedals, or is it just a learning curve thing?","ModifiedDate":"2007-12-17T00:00:00"}] |
As you can see, reviews are returned as a collection of JSON objects. There is exactly one row per each product so you do not need any transformation on the client side. This is a perfect choice if your client already expects JSON format for related records (e.g. if you are using some Angular, Knockout or other template engine that injects JSON model into HTML view).
As an alternative if you want to “join” products with related reviews as in the query above, you can use following query:
SELECT ProductID, Name, Color, ListPrice, ReviewerName, ReviewDate, Rating
FROM Production.Product
CROSS APPLY OPENJSON(Reviews)
WITH ( ReviewerName nvarchar(30) '$.Reviewer.Name',
ReviewDate datetime2,
Rating int)
WHERE Reviews IS NOT NULL
OPENJSON table value function takes related reviews formatted as JSON and returns them as a table. In the WITH part you can specify schema of returned table. Column names match names of the properties in JSON object, and if you have nested property (e.g. Reviewer.Name) you can specify JSON path where this value can be found. CROSS APPLY joins parent row with table returned by OPENJSON functions. Results are shown in the following table:
ID | Name | Color | ListPrice | ReviewerName | ReviewDate | Rating |
709 | Mountain Bike Socks, M | White | 9.50 | John Smith | 2007-10-20 00:00:00.000 | 5 |
937 | HL Mountain Pedal | Silver/Black | 80.99 | David | 2007-12-15 00:00:00.000 | 4 |
937 | HL Mountain Pedal | Silver/Black | 80.99 | Jill | 2007-12-17 00:00:00.000 | 2 |
798 | Road-550-W Yellow, 40 | Yellow | 1120.49 | Laura Norman | 2007-12-17 00:00:00.000 | 5 |
In both cases you don’t need to scan two tables – all necessary results are taken with a single query.
Summary
Although SQL Server is relational database you don't need to use strict relational theory in you information design. With new JSON support that is coming in SQL Server 2016 and Azure SQL Database you can choose when to follow strict concepts of relational schema design, and when to format objects as in the NoSQL database. SQL Server gives you flexibility to choose the best format for your applications.