Now that I have your attention, can you see what’s wrong with this query?
Prep
Like every food recipe out there, I’m going to make you sit through backstory before you can get to the good stuff.
One of the fun and annoying aspects of performance tuning is making sure that the results of the tuned version match the results of the pre-tuned version.
In this case, tuning went great – it’s amazing how much faster you can get things done when you don’t waste resources – but the results didn’t match.
Now, I was sure that the queries were logically the same, so I pinged a developer who had worked on the query last, and asked them to confirm what the query was supposed to do:
We want all records where the second table T2 has the specific value in ID2, or the first table T1 has the specific value in ID – Said developer
Well, great news for me! I made your query better and fixed a previous bug.
Now, can you figure out the bug?
Example
Here are your example tables: T1 and T2
create_sample_tables.sql
SQL
USE [tempdb];
GO
DROPTABLEIFEXISTS
[dbo].[T1],
[dbo].[T2];
CREATETABLE[dbo].[T1]
(
[ID]INTNOTNULL
);
CREATETABLE[dbo].[T2]
(
[ID1]INTNOTNULL,
[ID2]INTNULL
);
GO
Here’s some records for you:
populate_sample_tables.sql
SQL
USE [tempdb];
GO
INSERTINTO[dbo].[T1]
(
[ID]
)
VALUES
(1),
(2),
(3),
(4);
INSERTINTO[dbo].[T2]
(
[ID1],
[ID2]
)
VALUES
(1,1),
(1,2),
(2,2),
(3,1),
(3,2),
(4,NULL);
GO
Here’s what the expected outcome should be (and what it was post-tuning):
3 records
And here’s what the outcome actually was pre-tuning:
7 records
Buggy Code
buggy_code.sql
SQL
DECLARE @ID INT=1;
SELECT
[T1].[ID]AS[T1_ID],
[T2].[ID1]AS[T2_ID1],
[T2].[ID2]AS[T2_ID2]
FROM[dbo].[T1]
LEFTJOIN[dbo].[T2]
ON
(
[T1].[ID]=[T2].[ID1]
AND
[T2].[ID2]= @ID
)
OR
[T1].[ID]= @ID
WHERE
(
[T1].[ID]= @ID
OR
[T2].[ID1]ISNOTNULL
);
Sin É
So here’s the assignment!
Which line or lines in the code causes the bug?
Why?
It feels strange to sign off without explaining what the issue is, but I’ll return after a few days and fill this in.
Once people who love these kinds of questions get a chance to give it a go!
I filed that feat under the heading “Cool; Good to Know“, and then promptly forgot about it. Part of me thinks that I forgot about it because I didn’t understand how I accomplished it. While another part is sure I forgot about it cause “good to know” wasn’t on the list of tasks with looming deadlines.
I’m a man of many parts, but of more deadlines. Now, let’s understand it together!
First, there was the ooze
It’s a tactic of dealing with volatile amounts of data to call different stored procedures based on the number of records coming into it. I swear, I didn’t make this up!
Small numbers of records may be fine, but an odd, massive number may trigger a new plan. One that frequent executions and resources on the machine may not agree with.
So, normal, small, fast, very frequent executions get the “happy” path, but the big guy can go his own way. Free to roam, hog, and frizzle away everything he has!
We encountered this scenario once and decided to implement this tactic.
However, while reviewing this new stored procedure volatility countermeasure, I came across a temp table that wasn’t created anywhere in the procedure.
Stored Procedures are isolated. Aren’t they?
Or was it the sludge?
Let’s talk about Dynamic SQL for a moment.
I’m sure, although I won’t make up statistics, that you’ve used Dynamic SQL to query over different databases and collated their results.
Here’s a completely made-up example (Don’t do this. I’m sure it’s stupid, but it’s also very late right now)
SELECT COUNT(*) AS database_table_count FROM sys.tables
)
UPDATE #tables
SET table_count = database_table_count
FROM table_counts
WHERE tables_ID = @ID';
SELECT @max = MAX(tables_ID)FROM #tables;
WHILE @i <= @max
BEGIN
SET @context = CONCAT(
QUOTENAME(
(
SELECT TOP (1)[database_name]FROM #tables WHERE tables_ID = @i
)
),
@context_stub
);
EXEC @context @sql, N'@ID int', @ID = @i
SET @i +=1;
END;
SELECT*FROM #tables
I mean, look at line 25 in the code above. We didn’t create any temp table in that dynamic sql, but we’re updating it fine!
There was something I was forgetting: Scope!
Scopé (the é is pronounced)
I had never fully thought about this before.
I thought Dynamic SQL was just strings being executed (and it still kinda is), but it operates under a nested scope of the session.
So, when we say EXEC [sys].[sp_executesql] @stmt = N'INSERT INTO #temp…, for a temp table we’re created in the parent session, we’re ignoring scope boundaries.
The same thing happens with Stored Procedures.
Table Variables? No.
table_var_scope
SQL
CREATEORALTER PROC dbo.TableVar2
AS
BEGIN
INSERTINTO @table_variable (ID)VALUES(1);
END;
GO
CREATEORALTER PROC dbo.TableVar1
AS
BEGIN
DECLARE @table_variable TABLE(ID int);
EXEC dbo.TableVar2;
END;
GO
EXEC dbo.TableVar1;
Msg 1087, Level 15, State 2, Procedure TableVar2, Line 5 [Batch Start Line 0] Must declare the table variable “@table_variable”. The module ‘TableVar1’ depends on the missing object ‘dbo.TableVar2’. The module will still be created; however, it cannot run successfully until the object exists. Msg 2812, Level 16, State 62, Procedure dbo.TableVar1, Line 7 [Batch Start Line 17] Could not find stored procedure ‘dbo.TableVar2’.
Variables? No.
variables_scope
SQL
CREATEORALTER PROC dbo.Var2
AS
BEGIN
SET @i =2;
END;
GO
CREATEORALTER PROC dbo.Var1
AS
BEGIN
DECLARE @i int;
EXEC dbo.Var2;
END;
GO
EXEC dbo.Var1;
Msg 137, Level 15, State 1, Procedure Var2, Line 4 [Batch Start Line 0] Must declare the scalar variable “@i”. The module ‘Var1’ depends on the missing object ‘dbo.Var2’. The module will still be created; however, it cannot run successfully until the object exists. Msg 2812, Level 16, State 62, Procedure dbo.Var1, Line 7 [Batch Start Line 15] Could not find stored procedure ‘dbo.Var2’.
Temp Tables? Yes!
temp_table_scopes
SQL
CREATEORALTER PROC dbo.Proc1
AS
BEGIN
CREATETABLE #t1
(
ID intNOTNULL
);
INSERTINTO #t1 (ID)VALUES(1);
SELECT ID FROM #t1;
END;
GO
EXEC dbo.Proc1;
GO
CREATEORALTER PROC dbo.Proc2
AS
BEGIN
CREATETABLE #t2
(
ID intNOTNULL
);
EXEC dbo.Proc3;
SELECT ID FROM #t2;
END;
GO
CREATEORALTER PROC dbo.Proc3
AS
BEGIN
INSERTINTO #t2 (ID)VALUES(3);
END;
GO
EXEC dbo.Proc2;
ID 1, and ID 3
That’s what the temp table and the stored procedures were doing. Boundary violations don’t really count when it’s my nested scope!
It’s Actually a Donut
Now that that tangent is all squared away, let’s return to how I broke a First Responder Kit stored procedure.
CREATE OR ALTER PROC dbo.NormalProc
AS
BEGIN
CREATE TABLE #BlitzFirstResults
(
ID int NOT NULL
);
INSERT INTO #BlitzFirstResults VALUES (1);
SELECT ID FROM #BlitzFirstResults;
END;
GO
EXEC dbo.NormalProc;
GO
/*
DROP PROC dbo.NormalProc;
DROP TABLE IF EXISTS #BlitzFirstResults
*/
ID = 1 – happily, all day long
/*
DROP TABLE IF EXISTS #BlitzFirstResults
*/
IF OBJECT_ID(N'tempdb..#BlitzFirstResults') IS NULL
BEGIN
CREATE TABLE #BlitzFirstResults
(
BlitzFirstResults_ID int NOT NULL
);
END
SELECT * FROM #BlitzFirstResults;
EXEC dbo.NormalProc
(0 rows affected) Msg 207, Level 16, State 1, Procedure dbo.NormalProc, Line 11 [Batch Start Line 0] Invalid column name ‘ID’.
Different Columns Does Not A Stored Procedure Temp Table Boundary Violation Good Time Make
So, yeah … in truth, I didn’t so much break the stored procedure as I interfered with it…but that just doesn’t sound great when I say it like that.
Now, I know that my temp table in the parent session …got in the way… of the stored procedure’s inner temp table.
Anyway, things happened
What does this have to do with T-SQL Tuesday? Oh! The barest of passing similarities, but I’m team temp tables. They’re weird and (more) wonderful than I realised, but I’ll leave it to smarter people than myself to give you the raw numbers.
All I know is that I still have billion-row tables, procedures with multiple WHERE col1 = @param1 OR @param1 IS NULL) […] filters, and uptime requirements that make finding an outage window for UPDATE STATS more difficult than a greyscale Where’s Wally book.
Normally, I would have skipped this month because I’ve talked about permissions before, and I feel like it’s one of the better posts I’ve written. But I’m trying to write more, so that can’t be an excuse anymore.
So, let’s talk about AGs, DistAGs, and SQL Server authentication login SIDs.
AGs and DistAGs
In work, we have Availability Groups (AGs) and Distributed Availability Groups (DistAGs), depending on the environment level, e.g. Prod, Pre-Prod, QA, Dev, etc.
When we plan failovers for maintenance, we run through a checklist to ensure we can safely complete the failover.
One of these checks is to ensure that the correct logins with the correct SIDs are on each replica. Otherwise, we could get flooded with a lot of “HEY! My application doesn’t connect anymore! Why you break my application“, when apps try and log into the new primary and the user SIDs don’t match the login SIDs.
While I don’t have a problem with doing a query across registered servers, I threw together a quick script in PowerShell with dbatools that does the job of checking for me. And, like most things that happen in business, this temporary solution became part of our playbook.
Who knows! Maybe this quick, throwaway script could also become part of other people’s playbooks!
I’m not sure how I feel about that…
Scripts
We’re using dbatools for this because I think it’s the best tool for interacting with databases from a PowerShell session regardless of what Carbon Black complains about.
Then, we can use the ever-respected Get-DbaLogin to get a list of logins and their SIDs per replica. If we have a mismatch, we will have an issue after we failover. So best nip that in the bud now (also, I had this phrase SO wrong before I looked it up!).
$InstanceName = 'test'
# $InstanceName = $null # Uncomment this line to get ALL
$Servers = Get-DbaRegServer
if ($InstanceName) {
# Find the server that ends in our instance name
$Servers = $Servers | Where-Object ServerName -match ('{0}$' -f $InstanceName)
}
# Get all replicas per Instance name that are part of AGs...
$DAGs = Get-DbaAvailabilityGroup -SqlInstance $Servers.ServerName |
Where-Object AvailabilityGroup -notlike 'DAG*' | # Exclude DAGs which just show our underlying AG names, per our naming conventions.
ForEach-Object -Process {
$instance = $null
$instance = $_.InstanceName
foreach ($r in $_.AvailabilityReplicas) { # I'd like 1 replica per line, please.
[PSCustomObject] @{
InstanceName = $instance
Replica = $r.Name
}
}
} |
Select-Object -Property InstanceName, Replica -Unique |
Group-Object -Property InstanceName -AsHashTable
# Get a list of Logins/SIDs that don't match the count of replicas, i.e. someone forgot to create with SIDs...
foreach ($d in $DAGs.Keys) {
Write-Verbose "Working on instance: $d" -Verbose
Get-DbaLogin -SqlInstance $DAGs[$d].Replica |
Group-Object -Property Name, Sid |
Where-Object Count -ne $DAGs[$d].Count |
Select-Object -Property @{ Name = 'Instance'; Expression = { $_.Group[0].InstanceName }},
@{ Name = 'Occurances'; Expression = { $_.Count }},
@{ Name = 'Login/SID'; Expression = { $_.Name }}
}
I got 99 problems, and Login/SIDs are one! Or I have 5 Login/SID problems.
Cure
They say “prevention is better than cure”, and I’d love to get to a stage where we can “shift left” on these issues. Where we can catch them before someone creates a login with the same name but different SIDs on a replica.
But we’re not there yet. At least, we can find the issues just before they impact us.
Welcome to T-SQL Tuesday, the monthly blogging party where we are given a topic and have to talk about it. Today, we have Rob Farley ( blog | bluesky ), talking about integrity.
I’ll admit that it’s been a while since I’ve written a blog post. It’s been a combination of either burnout or busyness, but let’s see if I still have the old chops. Plus, I’m currently sick and resting in bed, so I have nothing better to do.
I’m one of the few who haven’t had experiences with corruption in our database. Apart from Steve Stedman’s ( blog ) Database Corruption Challenge, that is.
With that being said, what do I have to say about this topic then? Well, let’s talk about the first version of corruption checking automation that we introduced in work.
Now, this is just the bare bones and many different iterations since then, but the essence is here.
Overview
Like many shops out there, we can’t run corruption checking on our main production database instance. So, then, what do we do? We take the backups and restore them to a test instances, and then run corruption checking on those restored databases.
At least this way we can test the backups we take can be restored, as well.
But, I don’t want to spend every day manually restoring and corruption checking these databases, so let’s automate this bit…
Limitations
A quick scour of the interwebs brought back something extremely close to what I want by Madeira Data Solutions. It had some extras that I didn’t want, though.
More importantly, it used some functions that our dreaded antivirus software still screams false positives about. So, they would stop our script from running if we even tried.
A while back, I was tasked to performance tune some code that brought me through all five stages of grief. It’s best to take you through the events from the developer’s viewpoint. There’s less cursing that way.
Denial
Hey, what’s up? You were asked to troubleshoot some code. Not a big deal; these requests come in from time to time.
You have code that keeps coming up as a high consumer of CPU on our systems? OK?
It had gotten to the stage where it kept appearing in sp_WhoIsActive? What’s that? And should it not be “whom is active”? Well, agree to disagree.
Let’s see the code so. Wow, that’s a small scroll bar! Yeah, that’s one of ours.
No, it is. I swear it is.
I’m not surprised you can’t find it in stored procedures; we dynamically create it. Here’s the procedure. Yeah, all 900 lines! Pretty cool, huh?
What do you mean, why? We had to! We had to react to the different parameters that get passed in.
Anger
Alright, alright! Calm down. I misspoke.
Yeah, that’s it, breathe. There aren’t different parameters. It’s a parameter. Singular.
No, we still need to generate the query dynamically. Cause the XML could be different. Huh? Yeah, the parameter is XML; it could be anything in there. So we pass the XML in as a nvarchar(max) string.
You spilt some of your coffee there when you banged the desk.
Then we use sp_xml_preparedocument at the start, but then we have to use sp_xml_removedocument at the end.
You’ve never heard of those before? I thought you were a DBA?
We use the “prepare” to get the XML into a state that’s easier to consume, and we use the “remove” so we don’t get memory leaks!
Your face is getting a bit red, did you know that?
It’s SQL Server; it uses enough memory it can share some!
Did we read the docs? No, why? It can use one-eighth of the total memory available for SQL Server. Ah, but that’s “can”, not “will”.
Yes, yes, cursors upon cursors upon cursors. Why? We don’t know how many values are in each XML node in the params, so we have to split them out.
We then join them into a comma-delimited string, which is then used in IN clauses… woah! Is that the sound of your teeth grinding? You know that’s not good for your health. What do you mean neither am I?
Anyway, then we parse all of that down to a massive, what do you call it, swiss-army knife, lego-block, dynamic query built based on what’s passed in. You don’t call it that? I thought you did. What do you call it so? Wow, that’s not a word I had heard of before.
It’s not too bad, though! We pass in everything as literal values, so it’s faster that way. We read up on that parameter sniffing issue you said can happen. That isn’t going to catch us out here!
Modern problems require modern solutions, as they say. What’s that? It’s not a modern problem? I’m a what? Are you allowed to call me that?
Bargaining
You want me to re-write it? You’ll help? It’ll take a lot of time. It’s not really a priority.
Plus, it’s not even worth it. How much CPU? Is that a “illion” with an “m” or “illion” with a “b”? Per run? Is that a lot? Well, I really wouldn’t know, now would I?
Yeah, good point; you’ll also have to check how much memory it uses. Hey! Probably one-eight of the total memory available for SQL Server. Alright, it wasn’t that bad a joke; everyone’s a critic!
You’ll give me credit? But it’s fine in Dev; there’s no point. I suppose we could remove some of the WHERE OR IS NULL branches since we know if they’re empty when we create the query.
Nah, that would make the procedure a bit too big. Anything over 1000 lines is too much. Well, 985 is still less than 1000!
Depression
Why are you crying? No, we care, we do, we really do. No, not all the code is like this; this is old. Yes, we stopped using that XML parameter pattern. Yes, and the cursors. Well, most of them, at least.
Your tears are mixing with the coffee you spilt, you know that?
Here, there’s really no need for the fetal position in the middle of the office. You have yet to see the code for the busier databases.
You know what, why don’t you go home and come back in tomorrow, and I’ll go over that way, OK?
Acceptance
Hey! There’s our favourite DBA. You seem a lot cheerier.
Oh, you don’t have to perf-tune that query anymore? That’s great; see, it wasn’t so bad. What’s that? The latest release failed? It ran fine in Dev.
You’re permitted to set up a QA environment to test performance before code gets released? What kind of code? Any stored procedure?
Ah, that’s OK. No, really, it’s fine. We started using ORMs and embedding SQL into the app a while ago. This won’t affect us.
It has been quite a while since I have coded just for fun, so I’m thankful to Reitse for suggesting this. Unfortunately, I don’t have a pre-baked idea for this T-SQL Tuesday, so let’s see what we can come up with.
Echos
Around December 2021, Wordle hit the virtual scenes. Yeah, nearly two years ago. How do you feel about that?
I got swept up in that wave for a while in the same way I got swept up in the other trends of my time, like Pokemon, Sodoku, and Angry Birds.
Eventually, I stopped when I found a PowerShell script by Kieran Walsh ( github | twitter ) where you could narrow down to the correct answer by putting in the results of your guess each round.
This hack led to me realising how much time I was spending on Wordle and that I should stop, much like I did with Pokemon, Sodoku, and Angry Birds.
So, what better thing to do than to try and recreate that PowerShell script in T-SQL
Rules
I must recreate as much of the script as possible in T-SQL in only one hour. Yes, I’m aware that’s more of a rule than rules but Wordle needs five letters dammit, and “rule” was crying out for that prosthetic “s”!
Total (code)
Don’t worry, you just have to fill in the variables on lines 19-26.
Split
A few things need to be taken care of out of the bat.
The potential answers have to be stored somewhere in the database. Thankfully, I had the answers in a text file, so creating a table and then inserting them was easy.
I could do the insert with flat files, but I already have PowerShell open so…
Next, we need the variables that we can create. If I can finish this before the 1-hour mark, I’ll turn this into a stored procedure with parameters and everything! Until then, it’s script and variable times.
DECLARE
@known_letters AS varchar(5),
@excluded_letters AS varchar(26),
@position1 AS char(1),
@position2 AS char(1),
@position3 AS char(1),
@position4 AS char(1),
@position5 AS char(1),
@correct_letters AS xml,
@all_answers_sql AS nvarchar(MAX);
/* region Enter Variables here */
SET @known_letters = '';
SET @excluded_letters = '%[]%';
SET @position1 = NULL;
SET @position2 = NULL;
SET @position3 = NULL;
SET @position4 = NULL;
SET @position5 = NULL;
/* endregion Enter Variables here */
The PowerShell code has known_letters, excluded_letters, positions, and wrong_positions.
I can do all these easily enough, except for wrong_positions. I can’t think of a way to do hashtables in SQL that doesn’t equal a secondary table or user-table type, etc. I’ll leave that to the end if I have time.
known_letters is an array of strings. I haven’t updated the SQL Server version on my laptop in a while, so there is no string_split for me. Let’s do the XML way so.
/* region KnownLetters */
SELECT @correct_letters = CONCAT(
'<known_letters>',
REPLACE(@known_letters, ',', '</known_letters><known_letters>'),
'</known_letters>'
);
SELECT
[known] = [l].[y].value('.', 'char(1)')
INTO #KnownLetters
FROM
(
VALUES
(@correct_letters)
) AS [x] ([kl])
CROSS APPLY [kl].nodes('/known_letters') AS [l] (y);
/* endregion KnownLetters */
excluded_letters I can get away with by using some LIKE jiggery-pokery, where it will search for any characters between the square brackets.
positions I can split out into individual variables. I can more easily deal with them then, and it only ends up as an extra five variables this way.
Creating the table would have been handier if I had made a column for each character, but I didn’t, so it’s some SUBSTRING logic for me to get the individual characters out.
If we do know the positions of some of the letters, then I can strip out a lot of the potential answers straight away. I’m not a fan of Swiss-army-knife WHERE clauses, so I’ll do the dynamic SQL.
I’m also not a fan of WHERE 1=1 in my dynamic code, but I’m running low on time here, and it’s faster to add that in first and start everything else with an AND than it is to check if this is the first clause in the WHERE section or not.
Plus, I’m less against WHERE 1=1 than I am against Swiss-army-knife WHERE clauses.
/* region Known Positions */
CREATE TABLE #AllAnswers
(
[wordle_answers] char(5),
[char1] char(1),
[char2] char(1),
[char3] char(1),
[char4] char(1),
[char5] char(1)
);
SET @all_answers_sql = N'SELECT
[wa].[wordle_answers],
[g].[char1],
[g].[char2],
[g].[char3],
[g].[char4],
[g].[char5]
FROM [dbo].[WordleAnswers] AS [wa]
CROSS APPLY (
VALUES (
(SUBSTRING([wa].[wordle_answers], 1, 1)),
(SUBSTRING([wa].[wordle_answers], 2, 1)),
(SUBSTRING([wa].[wordle_answers], 3, 1)),
(SUBSTRING([wa].[wordle_answers], 4, 1)),
(SUBSTRING([wa].[wordle_answers], 5, 1))
)
) AS [g] ([char1], [char2], [char3], [char4], [char5])
WHERE 1=1';
IF @position1 IS NOT NULL SET @all_answers_sql = CONCAT(
@all_answers_sql,
N'
AND [g].[char1] = ',
QUOTENAME(@position1, '''')
);
IF @position2 IS NOT NULL SET @all_answers_sql = CONCAT(
@all_answers_sql,
N'
AND [g].[char2] = ',
QUOTENAME(@position2, '''')
);
IF @position3 IS NOT NULL SET @all_answers_sql = CONCAT(
@all_answers_sql,
N'
AND [g].[char3] = ',
QUOTENAME(@position3, '''')
);
IF @position4 IS NOT NULL SET @all_answers_sql = CONCAT(
@all_answers_sql,
N'
AND [g].[char4] = ',
QUOTENAME(@position4, '''')
);
IF @position5 IS NOT NULL SET @all_answers_sql = CONCAT(
@all_answers_sql,
N'
AND [g].[char5] = ',
QUOTENAME(@position5, '''')
);
SET @all_answers_sql = CONCAT(@all_answers_sql, N';')
PRINT @all_answers_sql;
INSERT INTO #AllAnswers
EXECUTE [sys].[sp_executesql] @stmt = @all_answers_sql;
/* endregion Known Positions */
Finally, we can UNPIVOT the individual characters for the words and join them with the known_letters to single down to those answers. As well as excluding characters that we know aren’t in the word.
Or else just return everything we have, minus excluded characters.
IF LEN(@known_letters) > 0 BEGIN
SELECT
*
FROM #AllAnswers AS [w]
UNPIVOT
(
[chars] FOR [chr2] IN ([w].[char1], [w].[char2], [w].[char3], [w].[char4], [w].[char5])
) AS [unpvt]
JOIN #KnownLetters AS [kl]
ON [unpvt].[chars] = [kl].[known]
WHERE
[unpvt].[wordle_answers] NOT LIKE @excluded_letters
END
ELSE
BEGIN
SELECT
*
FROM #AllAnswers AS [a]
WHERE [a].[wordle_answers] NOT LIKE @excluded_letters;
END;
Guilt
In the PowerShell script, you can add characters in the excluded_letters parameter that exist in the known_letters parameter, and it will correctly ignore them.
Alas, Tempus fugit and I didn’t get to do the same for this T-SQL version. Maybe that has something to do with translating “time flies” into Latin and then looking up other sayings in Latin, but we can’t say for sure. Mea culpa!
However, it’s been around 50 minutes with minor troubleshooting here and there, so time to test this bad boy.
I’ll take the first answer returned each time unless it is the answer we chose previously.
PowerShell
I’m not going to use the wrong_positions parameter here since I didn’t re-create that in T-SQL. Still, I got lucky and got the correct answer on the third guess
T-SQL
The T-SQL method doesn’t show each iteration as well as the PowerShell does. And, there’s more human brain power required to make sure you don’t enter the same letter in the known_letters and the excluded_letters variables.
Overall though, well done with a respectable four guesses
Point
I’m not going to say that there is no point to these exercises.
Fun is a valid a point as any other. In a work world filled with more demands on our time than the number of Pokemon (there’s still only 150, right?), more technologies to learn than combinations in Sodoku puzzles, and more people demanding the seemingly impossible out of you so that you want to yeet them at solid objects … something something Angry Birds, it’s a welcome change to do something just for fun once in a while.
You have to do some funky stuff to get the driver for SQL Server; granted, funky things basically mean run go get <Github link>. I’ve done funkier…
There’s also some better way to do this, but who cares. This is exploratory.
When setting up my SQL instances, I did some weird thing where I now have to specify a non-standard port when connecting. So I have to open up the error log in SQL Server, see what port it’s listening to, and put that into the code. Yes, I’m aware that I’m a DBA, and I should be able to fix this, but it’s my personal time & my personal test instance. I’ll fix it when I want to.
Anyway, here’s the code.
Do with it what you will; I’m just happy it works, and I’m delighted that I’m excited about it!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
And what happens when it fails, say cause the default port actively refuses connections?!
// changing `port = 60515` -> `port = 60514`
Happy out! Now to see what else we can do. Thankfully, I’ve somehow managed to get some ideas marinating in creative juices. Let’s see if I have the time and the energy to see them through!
I don’t know if this post is a rage against “database-ubiquitous” SQL or Object-Relational Mapping tools (ORMs)
I’m going to say that it’s a rant against the first because if you know what you are doing, then ORMs won’t be a performance concern
But people use ORMs instead of Stored Procedures because Them: “what happens if we need to change databases? We don’t have to worry about that now cause they’ll be baked in the application, and we won’t have to recreate the Stored Procedures in the new database cause ORMs just work!”
Everything just works until it doesn’t
I’ve worked with several companies and heard this argument many times from developers
Part of why I’m ranting about database-ubiquitousness SQL instead of ORMs is that I’ve also witnessed SQL code that has to work everywhere
I’m going to call this SQL “DUh SQL” from now on since I only have so many vowels on my keyboard and shouldn’t waste them re-writing the word “ubiquitousness”
Them: “Nope, we can’t use DATEFROMPARTS; it’s not available in other databases! Yeah, we use GETDATE() everywhere; what’s your point? WHAT!?!”
I’ve moved across different cloud providers more than I’ve moved databases, and guess what? Each cloud provider move, we’ve kept the same database
I know that knowledge sharing “in the wild” says that you should use ORMs or DUh SQL The route to a 10x engineer is paved in DRY, KISS, and SWALK Well, maybe not SWALK
Them: “But the leading minds out there say that you should keep it DUh since that means it’ll be a breeze if we have to move databases”
Sure, and I’ve been told that ice baths help in recovery I’m not going to take them, though, since I know there’s a difference between me trying to do a 5k and dedicated athletes!
I love arguing with people in my blog posts; I rarely lose
So, don’t use DUh SQL cause of reasons that may not apply to you Don’t refuse performance tuning efforts if they’ll add database-specific code And, please, learn to use your tools
Oh, and apologies for the lack of full-stops, I can’t use them in case I have to turn this post into regex
A Michal commented on the post, asking how to get a specific output from his search.
Hi,
Thanks for your sharing. What if I also want to compare case sensitively columns and the order of them (syncwindows). How can I presented it on powershell.
I mean that in the final table I want to show also something like: column_a, column_A –> case sensitive
AND
column_a, column_a –> different order in the table
Thanks in advance
Michal
I confess that I never got around to answering Michal until a few weeks ago when I found myself with some rare free time.
Since then, I’ve written a script, slapped it into a function, and threw it up on Github.
Here’s hoping that it does what you want this time Michal, thanks for waiting.
Shall I Compare Thee to Another Table?
The first thing that we need to do is have a couple of SQL tables to compare.
So, I threw up a Docker container and created a couple of tables with nearly the same layout.
You can see that there are around three differences here
Column orders, e.g. col9 has id 6 in dbo.DifferenceTable01 but id 5 in dbo.DifferenceTable02.
Column case sensitivity, e.g. col7 does not match COL7.
Column presence, e.g. col3 doesn’t exist in dbo.DifferenceTable01 at all.
While Compare-Object has the -CaseSensitive switch, I don’t think that it would be helpful in all these cases. Or else I didn’t want to use that command this time around.
So, I wrote a function to get the output we wanted, and yes, I now include myself among that list of people wishing for that output.
I’m allowed to be biased towards the things that I write 🙂
I’ve tried to include everything you could want in the function output, i.e. column names, column ids, and statuses.
Something I’ve started to do lately is wrapping a [Diagnostics.StopWatch] in my verbose statement to see where potential slow parts of the function are.
I’d like to think that 0.2 seconds for this example aren’t too bad.
Feel free to use and abuse this function to your hearts content. I know that there are a few things that I’d add to it. Comparing across different instances being an obvious one that I’d like to put in.
Hopefully though, someone out there will find it helpful.