Book Review: SQL Server 2012 High-Performance T-SQL Using Windows Functions

I’ve been extremely lazy about reading technical books for the past few months. Primarily I’ve been reading SQL blogs and haunting the SQLServerCentral forums instead. While still informative, I have a ton of SQL Ebooks that demand to be read. Part of my New Year’s Resolution was to start reading both technical books and novels again…so I’m on my fourth novel! However, I just finished my first technical book of the year. I read the first chapter of a few books before settling on something “easy”.

SQL Server 2012 High-Performance T-SQL Using Windows Functions by Itzik Ben-Gan is actually a very short for a SQL book, coming in at only 210 pages. With such a lengthy title, it’s shocking that the book is so short and concise. It is edited well, having a tiny errata containing only one technical mistake on a sample script and two minor typos. This is awesome considering many books I’ve read are plagued with gigantic errata. There is also a companion website where you can download source code and the entirety of Chapter 1.

TSQLFunctionsChapter 1 – SQL Windowing
Pages: 32

The introductory chapter provides a great overview of what the book has in store, making it a perfect sample download. It explains the options available for window functions and which functions are new in SQL 2012 (LAG, LEAD, etc.). The chapter provides some quick examples to whet your appetite with tantalizing hints of more details to come. The chapter is kept light enough that you want to keep reading while still providing useful information, all the while promising great things to come.

Highlights:

  • Programming options explained – Set Based, Window Functions, and Iterative
  • Preview of Chapter 5 in the form of sample scripts and situations
  • Elements of window functions – Partitioning, Ordering, and Framing

Chapter 2 – A Detailed Look at Windows Functions
Pages: 48

This chapter goes into explains partitioning and framing. If you are even slightly fuzzy on the concept, this is a very important section. I was surprised at just how much detail the author went into to explain the technicalities.  On the other hand, it’s a deep dive that could overwhelm someone trying to read the book with very little prior knowledge of window functions. In that case, I suggest you skim the chapter and come back to it after you better understand what window functions can do to save your queries.

The thing that bothers me is how the author melds theoretical SQL window functions in with the TSQL supported functions. I appreciate that he mentions ANSI standards, but if it’s not directly supported in TSQL, I don’t really want to learn about it in detail while reading about TSQL functions. Now I’m afraid that I will try using NTH_VALUE and then get horribly disappointed when I remember it’s not supported. Separating the functions that are not supported by SQL Server into a different section or chapter might have been the better route here.

Highlights:

  • Aggregate Functions – Distinct and Nested
  • Ranking Functions – Row_Number, Ntile, Rank, Dense_Rank
  • Distribution Functions – Percent_Rank, Cume_Dist
  • Offset Functions – Lag, Lead, First_Value, Last_Value

Chapter 3 – Ordered Set Functions
Pages: 20

The explanations in this chapter get very “mathy”. If equations scare you, you might need to read quickly, or just look at the general solutions and move on. Basically he provides script alternatives and the math necessary to get those alternatives to work. Very cool; a bit complex.

Highlights:

  • Hypothetical Set Functions – Ranking and Distributions
  • String Concatenation – Using Coalesce, Stuff, and XML path

Chapter 4 – Optimization of Windows Functions
Pages: 32

If you don’t understand query execution plans, a lot of this chapter will go over your head. It even starts with such a warning. There are numerous examples of execution plans and each example explains why it is slow or fast based on index scans, seeks, parallelism, etc. Sample data is provided for testing.

Columnstore indexes are mentioned, but for window functions to perform best, normally these should not be used due to sorting issues. It’s not like many people are using them yet anyways.

The detailed information on framing was my favorite part of this chapter. I’ve used ROWS UNBOUNDED PRECEDING to calculate running totals in SQL 2012 with great success, but I had never studied the particular differences between ROWS and RANGE (hint, stick with ROWS by default) or considered the many applications of the BETWEEN operator for framing, especially for pulling specific rows quickly using LEAD or LAG.

Highlights:

  • Indexing Guidelines for Window Functions
  • Improving Parallelism using Apply
  • Optimizing Functions using Framing (added in SQL 2012)
  • Distribution Functions – Rank and Inverse optimization

Chapter 5 – T-SQL Solutions Using Windows Functions
Pages: 78

Some people will skip directly to this chapter, and really, once you have read the book, this is most likely the section you will flip to when you need a trick query.  The format of this chapter is: Introduce an issue; provide a set based, iterative, and windows function solution; then compare the time and effort to do each.

It’s a great chapter to glance over once, and then keep mental notes of what it contains for when you need  to do some odd math, like calculate data islands or gaps.

Highlights:

  • Number Table
  • Date Sequences
  • Paging
  • Removing Duplicates
  • Pivoting
  • Top N
  • Mode
  • Running Totals
  • Intervals
  • Gaps
  • Islands
  • Median
  • Conditional Aggregate
  • Sorting Hierarchies

Summary

Overall, this was a great read that I recommend to anyone who writes TSQL. Although I’ve worked with window functions for a few years and have experience using the new SQL 2012 functions, the exhaustive detail that the author went into is important for tuning, especially considering the relatively short reading investment necessary. The last chapter will continue to be a great resource for rare query requests, especially since it  provides non-window function alternatives – in case a query has to run against an older version of SQL Server.

Mirroring with TDE

Recently I’ve had to set up a lot of database mirroring, and about half of those databases were encrypted. Setting up mirroring on unencrypted databases is normally a quick process, but encrypted mirroring requires a bit more work. While there are many examples of mirroring out there, it can be hard to find an example of encrypted mirroring that is not missing a step or two. You cannot use Object Explorer to do everything, so below are the necessary scripts to not only set up encrypted mirroring, but also how to setup Transparent Data Encryption.

1. Create and Backup Master Key and Certificate TDE requires a master key and a certificate, so those need to be created first and then immediately backed up. Backing these up is best practice, but the files will also need to be restored to the secondary server. If you already have TDE on the primary server, you can skip this step and go straight to Step 3.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/********** On Principal **********/

--Create Master Key
 USE MASTER
 GO
 CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'StongP@ssword'
 GO


--Backup Master Key
 BACKUP MASTER KEY TO FILE = 'FilePath\MasterKey.KEY'
 ENCRYPTION BY PASSWORD = 'StrongP@ssword'
 GO

--Create Certificate
 CREATE CERTIFICATE CertName
 WITH SUBJECT = 'CertDesc', EXPIRY_DATE = '1/1/2900'
 GO

--Backup Cert; using a different key name than the original to avoid errors
 BACKUP CERTIFICATE CertName
 TO FILE = 'FilePath\CertName.CER'
 WITH PRIVATE KEY (FILE='FilePath\CertKey.KEY',
 ENCRYPTION BY PASSWORD = 'StrongP@ssword')
 GO
 CLOSE MASTER KEY
 GO


2. Enable Transparent Data Encryption
 The next step is to enable TDE using the new certificate, and then turn TDE on. If TDE has been enabled previously, you should have already skipped to Step 3.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
--Enable TDE on database
USE DbName
GO
CREATE DATABASE ENCRYPTION KEY
WITH ALGORITHM = AES_256 ENCRYPTION BY SERVER CERTIFICATE CertName
GO

--Turn TDE On
ALTER DATABASE DbName
SET ENCRYPTION ON
GO


3. Restore Key and Certificate to Secondary
This is your first step if you already have TDE turned on. Just make sure you copy the backup files to the secondary server so you can restore them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/********** ON MIRROR **********/

--Restore Master key to Secondary server
USE MASTER
GO
RESTORE MASTER KEY
FROM FILE = 'FilePath\MasterKey.KEY'
DECRYPTION BY PASSWORD = 'StrongP@ssword'
ENCRYPTION BY PASSWORD = 'StrongP@ssword'
GO

--Restore the Certificate to Secondary server
OPEN MASTER KEY DECRYPTION BY PASSWORD = 'StrongP@ssword'
GO
CREATE CERTIFICATE CertName
FROM FILE = 'FilePath\CertName.CER'
WITH PRIVATE KEY ( FILE = 'FilePath\CertKey.key',
DECRYPTION BY PASSWORD = 'StrongP@ssword')
GO
CLOSE MASTER KEY
GO


4. Create Endpoints on Principal and Secondary
You can do this at any time that makes sense to you, but it makes sense to me to ensure all the settings are up to date before moving onto the mirroring setup. Encrypted mirroring requires an encrypted endpoint (seems logical enough) so notice the specified encryption algorithm in the script below. Many other mirroring examples leave this critical piece out.

1
2
3
4
5
6
7
8
/********** ON MIRROR & Principal **********/

--Create Mirroring Endpoint on Principal & Secondary
CREATE ENDPOINT Name
STATE = STARTED
AS TCP (LISTENER_PORT=5022)
FOR DATABASE_MIRRORING (ROLE=PARTNER,ENCRYPTION=SUPPORTED ALGORITHM AES)
GO


5. Backup Database on Principal
You will need to perform a full backup along with a Log backup on the database you plan to mirror. There is nothing special about the backup, just make sure that you have the latest log backups and that you finish before the next scheduled backup starts, otherwise you will have to restore another log. I hate having to do that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/********** On Principal **********/

--Backup Primary Database - Full
BACKUP DATABASE [DbName]
TO DISK = 'FilePath\DbNameFULL.BAK'
WITH NOFORMAT,INIT,COMPRESSION,SKIP,NOREWIND,NOUNLOAD,STATS=10,
NAME = 'DbFullBackup'
GO

--Backup Primary Database - Log
BACKUP LOG [DbName]
TO DISK = 'FilePath\DbNameLOG.TRN'
WITH NOFORMAT,INIT,COMPRESSION,SKIP,NOREWIND,NOUNLOAD,STATS=10,
NAME = 'DbLogBackup'
GO


6. Restore Backup to Secondary
Restoring the backups for an encrypted database is only slightly more involved than normal. You will have to Open the Master Key before the restore, otherwise it will fail.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/********** ON MIRROR **********/

--Restore the Full Backup on the Secondary
USE MASTER
GO
OPEN MASTER KEY DECRYPTION BY PASSWORD = 'StrongP@ssword' --Don't forget this!
RESTORE DATABASE [DbName]
FROM DISK = 'FilePath\DbNameFULL.BAK'
WITH NORECOVERY,NOUNLOAD,STATS=10
GO

--Restore the Log Backup on the Secondary
RESTORE LOG [DbName]
FROM DISK = 'FileName\DbNameLOG.TRN'
WITH NORECOVERY,NOUNLOAD,STATS=10
GO
CLOSE MASTER KEY --Remember to close the master key once complete
GO


7. Enable Mirroring on Secondary
 Once the database is in place, you may want to verify that the secondary database is in a Restoring state. This is required to mirror successfully. The scripts I’ve provided will restore correctly, but restoring the database improperly is one of the most common and embarrassing issues you’ll encounter while trying to turn on mirroring. Set the partner by using the fully qualified server name or by the IP address. Theoretically either will work, but it depends on your network.

The most important thing in the script below is to include the line to Add Encryption by Service. This can be done earlier and in a separate script, but I find it easiest to include while turning on mirroring. If you leave this step out, you’ll receive a misleading error after you run the script in Step 8 that will leave you depressed and frustrated because it won’t tell you anything useful. Something like:

Msg 1416, Level 16, State 31
Database "DbName" is not configured for database mirroring

So make sure you run the scripts with all the necessary lines!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/********** ON MIRROR **********/

--Enable Encrypted Mirroring on Secondary
USE MASTER
GO
OPEN MASTER KEY DECRYPTION BY PASSWORD = 'StrongP@ssword'
ALTER MASTER KEY ADD ENCRYPTION BY SERVICE MASTER KEY  --Most Important step!!
ALTER DATABASE DbName
SET PARTNER = 'TCP://PrincipalServer:5022'
GO
CLOSE MASTER KEY
GO


8. Enable Mirroring on Principal
This is the moment of truth. Did you follow all the steps correctly? If so, you should be fine. If you receive a database mirroring is not configured error, refer back to Step 7 and verify that encryption was added when you enabled mirroring. There is also a chance you will need to do another log restore to bring the log chain up to date. That’s purely based on how often you do log backups and how big the database is though.

1
2
3
4
5
6
7
8
/********** On Principal **********/

--Enable Encrypted Mirroring on Principal
USE MASTER
GO
ALTER DATABASE DbName
SET PARTNER = 'TCP://SecondaryServer:5022'
GO


Summary
Setting up Encrypted Mirroring is not overly difficult, but there are some quirks to it, and you can easily forget an integral step. I always refer to saved scripts to keep track of where I am in the process, and to make the overall setup faster.

Remote Registry Read/Write

Eventually you will need to verify registry keys on a list of computers, whether for security reasons, or some project. I find navigating the registry editor very tedious because of the sheer number of files that must be waded through. The endless collapsing and expanding…it’s just dreadful. Getting lost in a huge subfolder is one of my biggest annoyances. Registry read issues are further compounded when you have to deal with multiple computers. I find it much easier, and faster, to read and set values across many computers all at once using TSQL, or better yet, PowerShell.

SQL Server has a way to query and set values in the registry, although you may not realize it. This isn’t something everyone has to every day, but when you do, you want an easy way to handle it. The TSQL commands are actually relatively simple. XP_REGREAD and XP_REGWRITE can solve your read/write woes, but with limitations.

For all examples, I’ll be reading and writing values for UAC, which can be located in the registry under HKEY_LOCAL_MACHINE at Software\Microsoft\Windows\CurrentVersion\Policies\System controlled by the value EnableLUA. A value of 1 means UAC is On, while a value of 0 means UAC is Off.

To read a registry value using TSQL, run the following command:

EXECUTE MASTER.dbo.xp_regread
'HKEY_LOCAL_MACHINE' --HKEY directory
,'Software\Microsoft\Windows\CurrentVersion\Policies\System' --Registry Path
,'EnableLUA' --Registry Key

To set UAC On using TSQL, run the following command: (Set the last value to ‘0’ to turn UAC Off)

EXECUTE MASTER.dbo.xp_regwrite
'HKEY_LOCAL_MACHINE'
,'Software\Microsoft\Windows\CurrentVersion\Policies\System'
,'EnableLUA'
,'reg_sz' --Other options include reg_binary and reg_dword
,'1' --Key Value to set, in this case it would be 0 or 1

You may notice that the Results Pane indicates that 0 rows are affected. It made me angry when I tried to build these examples, but SQL Server is a liar here. Read the value of the registry key once again, and you will see that the value did update to your desired value. Flip the value on and off if you really don’t believe me. The problem with using TSQL lies in the limitations of SQL Server. Obviously you cannot read/write registry values of computers that do not have SQL running on them, and this includes passive nodes. How about PowerShell as a better solution?

PowerShell is a skill that is becoming more necessary as environments grow. I would argue that anytime you need to touch more than one computer, you should consider a remote solution using PowerShell. Plus, reading and writing to the registry just feels right when working through PowerShell.

The simple way of querying a registry value in PowerShell is as follows:

$Path = 'HKLM:Software\Microsoft\Windows\CurrentVersion\Policies\System'
$Key = 'EnableLUA'
Get-ItemProperty -Path $Path -Name $Key

However, unlike most Cmdlets, this one does not accept a ComputerName Property. Probably because Microsoft hates us. You can use a slightly more complicated method to query a remote registry though.

$Computer = "YourComputerHere"
$Path = 'Software\Microsoft\Windows\CurrentVersion\Policies\System'
$Key = 'EnableLUA'
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey = $Reg.OpenSubKey($Path)
$Value = $RegKey.GetValue($Key)
Write-Host "$Computer $Value"

Please note that I did not include HKLM: in the $Path variable this time. If you leave that in, you’ll receive an error stating that “You cannot call a method on a null-valued expression.” Using this script does not require the HK as it only deals with Registry Paths. The original Cmdlet we used, Get-ItemProperty, can get the properties of files, so it needs the extra distinction.

Next, we will expand the script slightly to include a ForEach statement in order to query multiple computers at once.

$ComputerList = "Computer1","Computer2"
ForEach ($Computer in $ComputerList)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey = $Reg.OpenSubKey($Path)
$Value = $RegKey.GetValue($Key)
Write-Host $Computer $Value
}

PowerShell includes an equally easy way to set the registry values, can you guess what the Cmdlet name is? Luckily that was a rhetorical question, and I will not make you answer in the comments. You can use Set-ItemProperty to update registry values. However, you are still limited to the local computer with this method. Let’s skip straight to the multi-server solution, as the baby steps are the same as those we took above with the Get statement.

$Newvalue = 1
ForEach ($Computer in $ComputerList)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$Computer)
$RegKey= $Reg.OpenSubKey($Path,$True)
$RegKey.SetValue($Key,$NewValue,[Microsoft.Win32.RegistryValueKind]::String)
$Value = $RegKey.GetValue($Key)
Write-Host $Computer $Value
}

I keep these scripts in a repository so I can quickly verify Registry Keys across my environment. PowerShell is a perfect solution for reading and writing settings remotely. There’s no limit to the number of bells and whistles you can add to PowerShell scripts,(try adding a Try-Catch for unreachable servers) but the important thing is getting the work done without having to touch every computer, one at a time. Don’t work hard; work smart, so you can read more blogs.

SQL Version Mismatch

Quote

I came across a peculiar issue a few days ago while patching SQL Server. A security scan alleged that one of the SQL Servers was under patched, meaning it was missing the latest required hotfix. A colleague jumped on the problem and began researching.

Probably the first thing we do to verify that SQL Server is running the correct version is to do a simple

SELECT @@VERSION

This of course returns the current version that your server is patched to.

SQL 2014 Test Lab Query
InitialVersion
I apologize for the sparse lab screenshots, but I was unable to recreate this issue quickly in a lab.

Everything looked great from the query, it matched the version that all of the other boxes were running, and the server was reporting that it had the mandated hotfix. Obviously this was a case of a scan that has just failed to connect to the box. A network glitch causing a false positive? Not the first time, won’t be the last. However, we have to prove beyond a shadow of a doubt that everything is in order, so the next step was jumping on the box itself and investigating.

The hotfix was confirmed to be in the installed updates. For good measure, my colleague verified that the SQL Installation Manager agreed. Booting up the hotfix installation file revealed that the SQL Instance was not reporting that the hotfix was installed, nor would it let us install it, but the Shared Services were fine, showing the correct recent patch. Whoa, what’s going on here?

By this time, things were starting to look dicey, and my colleague’s shift was ending. There was talk of doing horrifying things the next day if nothing was resolved. Things like repairing the SQL Installation or even a full Reinstallation. I offered to take a look as a fresh pair of eyes, not expecting to find anything, but just to get up to date on what was going on and as a last-ditch effort before wiping the world clean.

I started poking around and realized that the SQL Shared Services were registering as a different version than the SQL Server Instance. Well, that sounded bad. I thought that if we at least matched those versions up, we could get somewhere after that. It was agreed that this couldn’t hurt anything, so I uninstalled the more recent patch on the Shared Services to match with what the SQL Instance should have been.

Still nothing.

Ok, obviously there is something very wrong now. There had to be something we were missing.

And there was. Something simple and obvious.

In defeat, I finally took the time to just stare at the Installation Manager. I took a few moments to really read the whole screen.

That’s when I saw it. There is a box detailing the installed version of each SQL component on the box, and somehow everyone who had touched it overlooked the message here. The SQL Instance was reporting an incomplete update. Of course it would not let us patch the hotfix because it did not have the Cumulative Update required! It refused to install the Cumulative Update because it was reporting that SQL Server RTM was installed. No hot fixes. No service pack. Nothing. What?? How did no one catch this!

SQL Update for SQL 2014 as reference
WindowsUpdate

Once I saw that, I immediately started a service pack installation, if only to verify that it would be accepted. The Installation Manager was more than happy to oblige now that I wasn’t skipping versions. The necessary cumulative updates and hot fixes went smoothly after that.

In summation, please remember to slow down and really assess the problem at hand. Don’t jump into a problem trying to fix it before you know what is going on and possibly make matters worse. SQL is very good at providing hints, or in this case, blatant answers to your problems. You just need to step back and know where to look. Yes, we all had done SQL installs before, and yes, we all knew about the version information. In the heat of the moment though, we all tried to fire first and ask questions later for something that appeared like a simple problem.

I do find it disconcerting that a SQL Instance would report a patch level of the Shared Instances instead of the instance itself through SELECT @@VERSION. I tried to recreate the scenario in a lab, but SQL responded with the correct version each time. Whatever caused the incomplete installation caused the version information to report back incorrectly through SQL.

Once Upon a Time I Tried to Blog…

Every good story starts with, “Once Upon a Time.” Thus, it makes sense that a blog would to too.  Isn’t a blog essentially just a collection of short stories? A good one should be entertaining at least, and short enough so people will read it.

I’ve been fighting with starting this thing for months, and I finally took the dive to create a site this week as a joint adventure with my friend who runs Olympic Admin. We are both in the process of getting twitters and blogs spun up. Should be a wild ride.

Two days of planning later, and I am ready to start. (This is actually very quick for me, as I plan everything out very carefully, from purchases, to conversations)  I would have been even quicker at this if I did not get home from work today with a migraine that drained all of my energy. Such is life though. Moving on.

I wanted to start a technical blog in order to force myself to research SQL heavily. It should also be a good way to improve my writing skills. I plan to throw in my own twisted sense of humor along with some ranting about general IT projects, and perhaps a few blogs about role playing games. Eventually I hope this builds up my confidence to try public speaking at the local user group, but that’s another goal for later.

Setting up this blog, however, has been a horrible pain of indecision. I’ll explain why.

I Want Power: I actually hosted my own website for a few years in high school and college, and I think that is precisely the reason why it has been so hard to work with the free version of WordPress. I’m not used to having limitations on site layout. I don’t like where those headers are, I’ll just rearrange them…except I cannot find any way to edit the page settings! Seems nothing but the basics are free on WordPress, and I really mean basics. I can change the text and it taunts me with the ability to change colors, only to then demand a hundred dollars in return for a predetermined set of colors. I can only imagine it would cost ten times more for the ability to actually customize my colors. While I appreciate the simplicity of creation, there is really nothing allowed for free. It almost feels like a “Pay to Win” game. If this blogging thing works out, I will have to upgrade quickly because I want the ability to determine the full layout of my site. I want all the power.

I’m Lazy: I’ve been procrastinating about creating a blog for months, if not a year. Although I understand the technical benefit of writing and having to deep dive topics in order to look semi competent, the time requirement has long been a major deterrence. I like my free time, and I’m horribly lazy, so having to keep to a schedule, make updates, and write did not seem like much fun. Hopefully, I can keep my posts light-hearted enough to keep myself entertained while writing. If I entertain anyone who happens to read this, that is just a bonus.

Blogging is for Emo Kids: I still have the lingering mentality that blogs are juvenile wastes of time. Originally most people had blogs to rant about how dreadful their lives were. Remember Xanga? I do, sadly. Blogs still leave a bad taste in my mouth because of that, even after reading professional blogs for so long. Although I know a technical blog shouldn’t be like that, I cannot help but worry that my “Everything Else” title will creep into that dangerous zone of whining and crying like a stuck up child about not having enough candy and the sun not shining bright enough and how the world just isn’t fair. I mean, I complain about everything already, but I’ll try to not sound Emo about it.

I’m Not an Expert: This is the other big issue that has kept me from blogging. I’ve been in SQL for a relatively short period of time. Although I learn quick and I am relatively smart, there are many more people out there much more skilled than I am, with dozens more years of experience. With all those SQL bloggers out there, what could I possibly contribute, and how can I do so without looking foolish when I am not a master in the subject? Well, I have to keep telling myself I am going to blog to help myself learn, and share my professional growth. I’m not doing it to provide amazing insights to others. If I happen to provide some useful information to someone out there, awesome. I’ll be very proud, but if my blog is just for me, that is ok too. I’m going to do my best to blog about less common topics though, things that I have had to work on that has had relatively little documentation while searching the internet for answers.

Summary: I’m apprehensive about starting a blog, but I think it could be a fun endeavor, and it will help me grow both as a writer, and as a SQL professional. Welcome to SQL Sanctum.