Wie zit er aan mijn SQL Agent jobs?

De HiX Datawarehouse verversing van ChipSoft haalt de data uit een tijdelijk statische HiX secondary database op basis van logshipping op. Om de database statisch te maken, zorgt de software van HiX Datawarehouse voor de correcte afhandeling van de logship procedures. Het is voor gebruikers en beheerders nadrukkelijk niet de bedoeling om zélf de logship, met name de LSRestore job, in- of uit te schakelen. Maar… monitor dat maar eens! Microsoft SQL Server heeft daar zélf geen voorziening voor én het goede nieuws is dat deze voorziening eenvoudig zélf te schrijven is.

Functioneel
Wanneer een SQL Agent job in- of uitgeschakeld wordt, wordt dit in de tabel [dbo].[sysjobs] binnen de msdb database opgeslagen. Deze wijziging kunnen we afvangen en de wijziging kan in een aparte beheertabel worden vastgelegd.

Technisch
Met een AFTER UPDATE trigger op de tabel dbo.sysjobs binnen de msdb database wordt het mogelijk om de wijziging in de job status (ingeschakeld / uitgeschakeld) af te vangen. De gewijzigde informatie wordt vervolgens in een beheerdatabase / beheertabel opgeslagen, zodat hier later naar gekeken kan worden.

De code hiervoor …

USE msdb;
GO
CREATE TRIGGER [dbo].Alert_on_job_stat
   ON  [dbo].[sysjobs]
   AFTER UPDATE
AS 
BEGIN
DECLARE @old_status BIT, @new_status BIT, @job_name VARCHAR(1024)
 
SELECT @old_status = enabled FROM deleted
SELECT @new_status = enabled, @job_name = name FROM inserted
 
  IF(@old_status <> @new_status)
  BEGIN
    INSERT INTO [audit_database].dbo.[audit_tabel] (login_name, job_name, status_, datetime)
    VALUES(ORIGINAL_LOGIN(), @job_name, @new_status, GETDATE())
    
    DECLARE @body_content VARCHAR(1024) = '';
    SET @body_content = 'SQL job : ' + @job_name + ' has been ' + 
              CASE @new_status WHEN 1 THEN 'Enabled' ELSE 'Disabled' END 
              + ' @ ' + CAST(GETDATE() AS VARCHAR(30))

    /* Eventueel een e-mail sturen
    EXEC msdb.dbo.sp_send_dbmail  
    @profile_name = 'SQL Alert', 
    @recipients = 'beheerders@domeinnaam.com',  
    @subject = 'SQL job status Change Alert',  
    @body = @body_content
    */
    ;  
  END
END

Aandachtspunten:

  1. De database audit_database is een zelfgekozen naam en dient uiteraard te worden aangemaakt
  2. De tabelnaam is een zelfgekozen naam en dient uiteraard te worden aangemaakt
  3. E-mail is standaard door mij uitgeschakeld
  4. De e-mail instellingen moeten goed worden geconfigureerd
  5. Gebruikers die SQL Agent Jobs in en uit kunnen schakelen, dienen voldoende schrijfrechten op de audit database en audit tabel te krijgen

Met dank aan Jignesh Raiyani voor het idee en de uitwerking in het Engels; Link naar de webpagina

Veldlengtes (n)varchar en (n)char velden vs. query performance vs. server performance

Vandaag werd ik door mijn collega getriggerd om onderzoek te doen naar extreme lengtes in (n)varchar velden en de impact daarvan op performance. Welnu… riemen vast, want dit is belangrijk.

Management samenvatting
Een goede inrichting en adequate keuze van veldlengtes bij (n)varchar en (n)char velden draagt bij aan een optimaler query plan en daarmee aan een betere query en server performance in het algemeen.

Hoe zit dit precies
U kent het wel: “Laten we dat tekstveld alvast maar wat langer maken, zodat we ook in de toekomst deze velden niet zo snel hoeven te verlengen”. Die beslissing kan impact hebben op de query performance van uw query’s op deze velden. SQL Server kijkt bij het maken van het query plan (die SQL Server bij query’s gebruikt) naar de grootte van de tekstvelden die in de query wordt gebruikt. Op basis daarvan maakt SQL Server een inschatting van het benodigd geheugen. Hoe meer de veldlengte van het veld in de tabel afwijkt van de daadwerkelijke vulling des te meer geheugen zal SQL Server onnodig aan deze query toewijzen. Gelukkig zal SQL Server (2019) u hier in het Actual Query Plan op wijzen bij het meest linkse (SELECT) component. U vindt in het Actual Query Plan op dat component een geel driehoekje met een waarschuwing.

Eén en ander is met onderstaand TestLab verduidelijkt. Deze set met query’s helpt u hopelijk beter te begrijpen wat er precies fout gaat. Dit is overigens geen ‘incident’. De onjuiste bepaling van het benodigd geheugen zal continue fout gaan wanneer de betrokken tabel (of, indien dit zich in meerdere tabellen voordoet, betrokken tabellen) in een query wordt betrokken. Draaien er parallel meerdere query’s, dan zal dit potentieel onnodig veel resources op het geheugen van uw SQL Server gaan leggen; en geheugen is schaars en kostbaar.

Vragen staat vrij!


/*
Auteur : Mickel Reemer
Datum : 23 maart 2022
Thanks to : Erik Darling
Credits by : https://www.brentozar.com/archive/2017/02/memory-grants-data-size/
SQL Server : SQL Server 2019 Developer Edition

Doel van het script:
--------------------
Aantonen dat varchar die te groot zijn aangemaakt
oorzaak zijn excessive memory grants op een SQL Server
*/
-- Zet tijdstatistieken aan
SET STATISTICS TIME ON

-- Verwijder de testtabel wanneer deze al bestaat
DROP TABLE IF EXISTS dbo.MemoryGrants;

-- Maak de testtabel aan
CREATE TABLE dbo.MemoryGrants
(
ID INT PRIMARY KEY CLUSTERED,
Ordering INT,
Field1 VARCHAR(10),
);

-- Voeg de records in, in de tabel
INSERT dbo.MemoryGrants WITH (TABLOCK)
( ID, Ordering, Field1)
SELECT
c,
c % 1000,
REPLICATE('X', c * 10 % 10)
FROM (
SELECT TOP (100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS c
FROM sys.messages AS m
CROSS JOIN sys.messages AS m2
) x

-- Zet Actual Query Plan aan en draai de query hieronder
SELECT mg.ID, mg.Ordering, mg.Field1 --Returning a VARCHAR(10)
FROM dbo.MemoryGrants AS mg
ORDER BY mg.Ordering
GO
-- De doorlooptijd is 1751ms. De memory grant is 13MB

/*
Dat gaan we nu nog een keer testen, maar dan met varchar(8000)
*/

-- Verwijder de testtabel opnieuw wanneer deze bestaat
DROP TABLE IF EXISTS dbo.MemoryGrants;

-- Maak de testtabel aan
CREATE TABLE dbo.MemoryGrants
(
ID INT PRIMARY KEY CLUSTERED,
Ordering INT,
Field1 VARCHAR(8000), -- We gaan nu Field1 een lengte van 8000 geven
);

-- Voeg de records in, in de tabel
INSERT dbo.MemoryGrants WITH (TABLOCK)
( ID, Ordering, Field1)
SELECT
c,
c % 1000,
REPLICATE('X', c * 10 % 10)
FROM (
SELECT TOP (100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS c
FROM sys.messages AS m
CROSS JOIN sys.messages AS m2
) x

-- Zet Actual Query Plan aan en draai de query hieronder
SELECT mg.ID, mg.Ordering, mg.Field1 --Returning a VARCHAR(10)
FROM dbo.MemoryGrants AS mg
ORDER BY mg.Ordering
GO
-- De memory grant is 574MB. De doorlooptijd is 2706ms

Ongebruikte indexen op tabellen in een database vinden

Het is als database administrator belangrijk periodiek te kijken of er indexen op tabellen staan die door SQL Server nooit meer worden gebruikt. Dit voorkomt dat deze indexen onnodig schijfruimte innemen en/of onnodige overhead (performance) veroorzaken bij het bewerken, toevoegen of verwijderen van records op de tabel. In deze video wordt daar uitgebreider door mij op ingegaan en wordt getoond op welke wijze hier het best een analyse op kan worden gemaakt.

Het bijbehorende script is hier te downloaden.

Overzicht open transacties in SQL Server

Stel je voor dat je op een SQL Server in een database tabel binnen transacties data aan het bewerken bent. Het is dan handig te weten of er nog open transacties zijn, die nog definitief moeten worden gemaakt of terug moeten worden gedraaid. Ook wanneer wordt geprobeerd om de transactielog vrij te maken, is het handig te weten of er nog transacties nodig zijn. Er zijn een aantal manieren om inzicht te krijgen in de transacties die nog open staan.

    DBCC OPENTRAN
    Deze functie wordt vooral gebruikt bij het vrijmaken van ruimte in de transactielogs. Het commando geeft de oudste transactie aan die nog open staat voor verwerking.
    Belangrijk: dit commando geeft alleen informatie over de actief geselecteerde database waarbinnen deze instructie wordt uitgevoerd.

    sys.sysprocesses
    De query …

    SELECT
    * 
    FROM sys.sysprocesses
    WHERE open_tran <> 0
    

    … geeft informatie over open transacties. Deze system view (dus geen tabel!) geeft onder andere informatie over het proces ID waaronder de transactie draait, op welke machine de transactie is gestart ook met welk account. Deze tabel is vooral handig om te identificeren wie, welke transacties open heeft.
    Belangrijk: de informatie wordt serverbreed weergegeven (en dus niet per databases)

    sys.dm_tran_active_transactions
    Onderstaande handige query is een query op de serverbrede view (dus geen tabel!) sys.dm_tran_active_transactions. Deze tabel wordt voornamelijk gebruikt om de status en voortgang van verschillende open transacties in kaart te brengen. De velden zijn in onderstaande query alvast vertaald naar betekenissen (in het Engels).

    select transaction_id, name, transaction_begin_time
     ,case transaction_type 
        when 1 then '1 = Read/write transaction'
        when 2 then '2 = Read-only transaction'
        when 3 then '3 = System transaction'
        when 4 then '4 = Distributed transaction'
    end as transaction_type 
    ,case transaction_state 
        when 0 then '0 = The transaction has not been completely initialized yet'
        when 1 then '1 = The transaction has been initialized but has not started'
        when 2 then '2 = The transaction is active'
        when 3 then '3 = The transaction has ended. This is used for read-only transactions'
        when 4 then '4 = The commit process has been initiated on the distributed transaction'
        when 5 then '5 = The transaction is in a prepared state and waiting resolution'
        when 6 then '6 = The transaction has been committed'
        when 7 then '7 = The transaction is being rolled back'
        when 8 then '8 = The transaction has been rolled back'
    end as transaction_state
    ,case dtc_state 
        when 1 then '1 = ACTIVE'
        when 2 then '2 = PREPARED'
        when 3 then '3 = COMMITTED'
        when 4 then '4 = ABORTED'
        when 5 then '5 = RECOVERED'
    end as dtc_state 
    ,transaction_status, transaction_status2,dtc_status, dtc_isolation_level, filestream_transaction_id
    from sys.dm_tran_active_transactions
    

    Belangrijk: de informatie wordt serverbreed weergegeven (en dus niet per databases)

    Credits
    Met dank aan: Tharif, Sebastian Brosch en Alisson Gomes
    Gebruikte link op StackOverflow: hier
    Microsoft Docs voor meer informatie: hier

Foutmelding in kubusverwerking via een SQL Agent Job

UItgangspunt
Er is een kubusdatabase waarbij meetwaarden worden gevonden die een onbekende dimensiewaarde hebben. Om te voorkomen dat dit een foutmelding tijdens het bijwerken van de data (process database) plaatsvindt, worden in het tabblad “Dimension key errors” binnen Batch Settings Summary gewijzigd. Daarbij worden de volgende keuzes gemaakt:

– Bij fouten in dimensies worde de waarde naar ‘unknown’ geconverteerd
– Er worden in totaal maximaal 500 dimensiefouten geaccepteerd
– Wanneer de dimensiewaardie niet wordt gevonden, gaat het bijwerken van de kubus door
– Wanneer een niet toegestane Null key wordt gevonden, gaat het bijwerken ook door

Grafisch:

Wanneer de kubus op deze wijze via een SQL Agent job wordt bijgewerkt (process database) zal de SQL Agent job met de volgende melding falen:

<Warning WarningCode=”1092354050″ Description=”Server: Operation completed with … problems logged.” … />

De SQL Agent job geeft dus een harde foutmelding. Wees er dus op bedacht dat het aantal en soort fouten bij het instellen van de kubusverversing zijn geaccepteerd en dat de SQL Agent job daarmee eigenlijk onterecht faalt.

Omdat de SQL Agent een onterechte fout geeft, is er een stukje maatwerkcode nodig om de jobs periodiek op juistheid en volledigheid te controleren. Onderstaand T-SQL kan hiervoor worden gebruikt. U zult zelf even moeten kijken welke velden uit deze tabellen bruikbaar voor u zijn.

SELECT
*
FROM msdb.dbo.sysjobs j 
INNER JOIN msdb.dbo.sysjobsteps s ON j.job_id = s.job_id
INNER JOIN msdb.dbo.sysjobhistory h ON s.job_id = h.job_id AND s.step_id = h.step_id

 

SQL Server foutmelding “String or binary data would be truncated” met trace flag 460 analyseren

Wellicht dat u de foutmelding herkent. Met deze foutmelding begint de eindeloze zoektocht naar het juiste veld en record waar dit probleem zich in voordoet.

Eindelijk heeft Microsoft een oplossing hiervoor gemaakt, te weten trace flag 460. Wanneer deze trace flag wordt aangezet, geeft SQL Server meer informatie over de foutmelding, namelijk het veld waar het probleem zich voordoet en het gevonden record.

Ik verwacht dat ontwikkelaars hier de nodige tijd mee zullen besparen. Deze trace flag is beschikbaar in SQL Server 2016 SP2, CU6 en SQL Server 2017, CU12

Meer informatie in deze link van Microsoft: https://support.microsoft.com/en-us/help/4468101/optional-replacement-for-string-or-binary-data-would-be-truncated. Voorbeelden in het Engels zijn beschikbaar via Brent Ozar: https://www.brentozar.com/archive/2019/03/how-to-fix-the-error-string-or-binary-data-would-be-truncated/.

T-SQL – SELECT TOP 100 vs SELECT TOP 101

SQL Server heeft een fijne manier van sorteren. Een SELECT TOP 100 wordt door SQL Server veel efficiënter gesorteerd dan een SELECT TOP 101+. In de StackOverflow database wordt dat duidelijk, wanneer de ‘Actual Query Plan’ wordt aangezet en onderstaande query’s worden uitgevoerd

SELECT TOP 100
*
FROM dbo.Users
ORDER BY Reputation DESC
GO

SELECT TOP 101
*
FROM dbo.Users
ORDER BY Reputation DESC
GO

Het verschil in aanpak door SQL Server wordt duidelijk door te kijken naar de memory grant.

Performance: Werkelijke data in plaats van cached plans

Samenvatting:
SQL Query’s worden als tekst in de cache van de server opgeslagen om later eventueel te worden hergebruikt. De kleinste afwijking in een query, levert een nieuw record van de query in de cache op; dus ook bij een andere formatting, het gebruik van hoofdletters of het gebruik van comments. Query plannen zijn erg belangrijk en het opslaan van query plannen komt de performance ten goede. Echter, query plannen dubbel in de cache opslaan omdat de tekst nét wat anders eruit ziet, gaat ten koste van het geheugen, waardoor er minder ruimte voor werkelijke data overblijft, die regelmatig wordt benaderd.

Uitwerking en voorbeeld
Om query’s zo efficiënt mogelijk te draaien, slaat SQL Server query plannen op. Deze worden opgeslagen in de tabellen sys.dm_exec_cached_plans (de plannen) en sys.dm_exe_sql_text (de tekst van de SQL queries). Het opslaan van deze query plannen gebeurt in het geheugen. In principe is hier niets mis mee. SQL Server gebruikt deze query plannen opnieuw wanneer dezelfde query wordt gedraaid en hoeft niet opnieuw een query plan op te zetten, wat performance kost.

Wat regelmatig vergeten wordt, is dat query plannen als text worden opgeslagen. Hierdoor is het mogelijk dat een query plan van dezelfde query twee keer in de cache voorkomt. Voorbeeld van een query, uitgevoerd op de StackOverflow database:

/* Test query 1 */
SELECT
*
FROM dbo.Users
WHERE Reputation = 2499
GO

/* Test query 2 */
SELECT
*
FROM dbo.Users
WHERE Reputation = 2499
GO

Wordt vervolgens de cache uitgelezen met de volgende query:

SELECT
	cacheobjtype,
	text,
	plan_handle
FROM sys.dm_exec_cached_plans P
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
WHERE text LIKE '%Test query%' AND text NOT LIKE '%sys.dm_%'

Dan is het resultaat twee records. Wordt er een ‘Test query 3’ toegevoegd, dan zitten er daarna drie records in de cache. Onbedoeld kan het geheugen hiermee onnodig worden belast met query plannen van dezelfde query. Dit terwijl het geheugen juist zo vol mogelijk moet worden gezet met daadwerkelijke data uit de database die regelmatig wordt benaderd.

Conclusie: voorzichtig, dus! SQL Server houdt zelfs rekening met hoofdletters. Is er één teken in de query tekst anders (hoofdletter, punt, komma, enter, wat dan ook!), dan verschijnen er al twee query plannen in de cache.

Uitdagingen bij alleen schrijfrechten en de WHERE clausule

Bron en credits: https://sqlstudies.com/2016/08/11/write-only-permissions/
Gebruikte demo database: AdventureWorks 2014

Tijdens het verzamelen van informatie kwam ik een interessante reproductie tegen van een probleem bij het uitgeven van schrijfrechten zonder leesrechten. Deze situatie kan zich onder andere voorden bij het ontwikkelen van applicaties, waarbij de applicatie uitsluitend tabellen mag bijwerken, zonder dat tabellen hoeven te worden gelezen. Daar zit een behoorlijke adder onder het gras waar bij het uitrollen van de beveiliging van de database wel rekening mee moet worden gehouden.

Het scenario werkt als volgt: een applicatie draagt uitsluitend zorg voor het bijwerken van velden in een database. Theoretisch zou de rol db_datawriter rechten dan voldoende moeten zijn. Laten we een dergelijke gebruiker aanmaken en aan de database AdventureWorks 2014 koppelen:

-- Setup code
CREATE LOGIN WriteOnlyUser WITH PASSWORD = 'WriteOnlyUser',CHECK_POLICY = OFF;
GO
USE AdventureWorks2014;
GO
CREATE USER WriteOnlyUser FROM LOGIN WriteOnlyUser;
GO
ALTER ROLE db_datawriter ADD WriteOnlyUser;
GO

De gebruiker WriteOnlyUser heeft nu uitsluitend update rechten op de database AdventureWorks.

EXECUTE AS USER = 'WriteOnlyUser';
GO
INSERT INTO Person.PersonPhone (BusinessEntityID, PhoneNumber, PhoneNumberTypeID) 
    VALUES (1,'999-999-9999',1);
GO
REVERT;
GO

Werkt prima! Mooi! Met andere woorden de gebruiker WriteOnlyUser kan de tabel in de database aanvullen. Nu vraagt de applicatie om één record in de tabel aan te passen:

EXECUTE AS USER = 'WriteOnlyUser';
GO
UPDATE Person.PersonPhone SET PhoneNumberTypeID = 3
    WHERE BusinessEntityId = 1
      AND PhoneNumber = '999-999-9999';
GO
DELETE Person.PersonPhone
    WHERE BusinessEntityId = 1
      AND PhoneNumber = '999-999-9999';
GO
REVERT;
GO

Msg 229, Level 14, State 5, Line ##
The SELECT permissions was denied on the object ‘PersonPhone’, database ‘AdventureWorks 2014’

Eh… wat??? Waarom verschijnt hier een leesfout, terwijl gevraagd wordt de tabel in de database bij te werken? Wat helemaal gek is, is dat dit wel werkt:

EXECUTE AS USER = 'WriteOnlyUser';
GO
UPDATE TOP (1) Person.PersonPhone SET PhoneNumberTypeID = 3;
GO
DELETE TOP (1) Person.PersonPhone;
GO
REVERT;
GO

Het antwoord
Een WHERE clausule is een leesactie. Immers, de tabel moet gelezen worden om te bepalen welke records aan de voorwaarde voldoen. Bij het inrichten van de rechtenstructuur is het dus belangrijk met de WHERE clausule rekening te houden. Er is geen workaround voor dit probleem. Ik kan mij geen situaties voorstellen waarin applicaties structureel alle records in een tabel wilt bijwerken of verwijderen. Je ontkomt er dan bijna niet aan om de rechten voor de applicatie met leesrechten uit te breiden.

Bitwise operators in SQL Server: Bitwise Exclusive OR

Bron: Microsoft TechNet
Download: het gehele behandelde script uit deze bijdrage kan hier worden gedownload.

Met bitwise operators kan data op bit niveau worden geëvalueerd. Hoewel deze techniek al sinds SQL Server 2008 beschikbaar is, is deze techniek nieuw voor mij. In deze bijdrage wil ik graag dieper ingaan op de functie Bitwise Exclusive OR en een handige toepassing van deze functie. Microsoft beschrijft deze specifieke functie op deze Technet pagina.

De functie bitwise Exclusive OR vergelijkt twee expressies op bit niveau. Het resultaat wordt per bit als volgt bepaald:
1 Wanneer op bitniveau één van de bits het resultaat 1 laat zien, is het resultaat van de Exclusive OR 1
2 Wanneer op bitniveau beide bits 1 of 0 zijn, is het resultaat van de Exclusive OR 0

Waarom bitwise operators?
Omdat er op bitniveau wordt gewerkt, kan afhankelijk van de complexiteit van de query de performance bij gebruik van de bitwise operators significant beter zijn dan het gebruik van traditionele oplossingen, zoals een samengestelde WHERE clause of het gebruik van CASE WHEN.

Voorbeeld van een bitwise operator met één bit
Een datum kan wel (1) of geen (0) feestdag zijn. Met de Exclusive OR is dit eenvoudig vast te stellen: (1 ^ Feestdag)

Verklaring
Is de datum een feestdag (dus 1), dan is de uitkomst van de Exclusive OR (1 ^ 1) dus 0, immers op bitniveau zijn beide bits gelijk aan elkaar (voorwaarde 2)

Toepassing
Natuurlijk is het gebruik in bovenstaand voorbeeld wat triviaal, immers Feestdag = 1 geeft hetzelfde resultaat. Het wordt een ander verhaal wanneer er twee tests uitgevoerd moeten worden, bijvoorbeeld in het geval bepaald moet worden of een datum een werkdag is (dus geen feestdag en geen dag in het weekeinde). Wanneer in de datumtabel geen veld ‘IsWerkdag’ aanwezig is, maar de velden ‘IsFeestdag’ (bit) en ‘IsWeekenddag’ (bit) wel, zijn met de Exclusive OR functie werkdagen eenvoudig vast te stellen. Onderstaande query illustreert dit:

-- Gebruik een testdatabase (niet in productie)
USE TestDB
GO
-- Aanmaken tijdelijke tabel
CREATE TABLE Datumtabel (
Datum datetime,
Dag char(20),
IsWeekenddag bit,
IsFeestdag bit
)
GO

-- Tabel vullen
INSERT INTO Datumtabel (Datum, Dag, IsWeekenddag, IsFeestdag)
VALUES
('2016-12-24 00:00:00.000','Zaterdag',1,0),
('2016-12-25 00:00:00.000','Zondag',1,1),
('2016-12-26 00:00:00.000','Maandag',0,1),
('2016-12-27 00:00:00.000','Dinsdag',0,0),
('2016-12-28 00:00:00.000','Woensdag',0,0),
('2016-12-29 00:00:00.000','Donderdag',0,0),
('2016-12-30 00:00:00.000','Vrijdag',0,0),
('2016-12-31 00:00:00.000','Zaterdag',1,0),
('2017-01-01 00:00:00.000','Zondag',1,1),
('2017-01-02 00:00:00.000','Maandag',0,0)
GO

-- Voorbeeldquery om werkdag te bepalen
SELECT
Datum,
Dag,
IsWeekenddag,
'(1 ^ IsWeekenddag), dus 1 ^ ' + CONVERT(char(1), IsWeekenddag) +
' = ' + CONVERT(char(1),(1 ^ IsWeekenddag)) +
' ' + CASE WHEN (1 ^ IsWeekenddag) = 0 THEN '(Gelijk)'
ELSE '(Niet gelijk)' END AS BWEO_Feestdag,
IsFeestdag,
'(1 ^ IsFeestdag), dus 1 ^ ' + CONVERT(char(1), IsFeestdag) +
' = ' + + CONVERT(char(1),(1 ^ IsFeestdag)) +
' = ' + CONVERT(char(1),(1 ^ IsFeestdag)) +
' ' + CASE WHEN (1 ^ IsFeestdag) = 0 THEN '(Gelijk)'
ELSE '(Niet gelijk)'END AS BWEO_Weekenddag,
CONVERT(char(1),(1 ^ IsWeekenddag)) + ' (BWEO_Feestdag) & ' +
CONVERT(char(1),(1 ^ IsFeestdag)) +' (BWEO_Weekenddag = ' +
CONVERT(char(1),(1 ^ IsFeestdag) & (1 ^ IsWeekenddag)) AS BepalingWerkdag,
CONVERT(char(1),(1 ^ IsFeestdag) & (1 ^ IsWeekenddag)) +
' -- (' + RTRIM(Dag) + ' en ' + CASE WHEN IsFeestdag = 1 THEN 'een feestdag)'
ELSE 'geen feestdag)' END AS Werkdag,
(1 ^ IsFeestdag) & (1 ^ IsWeekenddag) AS IsWerkdag -- uiteindelijk resultaat van de bepaling van de werkdag
FROM Datumtabel
-- WHERE (1 ^ IsFeestdag) & (1 ^ IsWeekenddag) = 0 -- Alle niet werkdagen
-- WHERE (1 ^ IsFeestdag) & (1 ^ IsWeekenddag) = 1 -- Alle werkdagen
ORDER BY Datum
GO

Wanneer de query op traditionele wijze had worden uitgevoerd, zou de query voor het ophalen van de niet werkdagen er als volgt eruit hebben gezien:

SELECT
Datum,
Dag,
IsWeekenddag,
'(1 ^ IsWeekenddag), dus 1 ^ ' + CONVERT(char(1), IsWeekenddag) +
' = ' + CONVERT(char(1),(1 ^ IsWeekenddag)) +
' ' + CASE WHEN (1 ^ IsWeekenddag) = 0 THEN '(Gelijk)'
ELSE '(Niet gelijk)' END AS BWEO_Feestdag,
IsFeestdag,
'(1 ^ IsFeestdag), dus 1 ^ ' + CONVERT(char(1), IsFeestdag) +
' = ' + + CONVERT(char(1),(1 ^ IsFeestdag)) +
' = ' + CONVERT(char(1),(1 ^ IsFeestdag)) +
' ' + CASE WHEN (1 ^ IsFeestdag) = 0 THEN '(Gelijk)'
ELSE '(Niet gelijk)'END AS BWEO_Weekenddag,
CONVERT(char(1),(1 ^ IsWeekenddag)) + ' (BWEO_Feestdag) & ' +
CONVERT(char(1),(1 ^ IsFeestdag)) +' (BWEO_Weekenddag = ' +
CONVERT(char(1),(1 ^ IsFeestdag) & (1 ^ IsWeekenddag)) AS BepalingWerkdag,
CONVERT(char(1),(1 ^ IsFeestdag) & (1 ^ IsWeekenddag)) +
' -- (' + RTRIM(Dag) + ' en ' + CASE WHEN IsFeestdag = 1 THEN 'een feestdag)'
ELSE 'geen feestdag)' END AS Werkdag,
(1 ^ IsFeestdag) & (1 ^ IsWeekenddag) AS IsWerkdag
FROM Datumtabel
WHERE (IsFeestdag = 1 AND IsWeekenddag = 0)
OR (IsFeestdag = 0 AND IsWeekenddag = 1)
OR (IsFeestdag = 1 AND IsWeekenddag = 1) -- Alle niet werkdagen
ORDER BY Datum
GO

-- De rommel achter onze kont opruimen
DROP TABLE Datumtabel

Door gebruik te maken van execution plan, live query plan en IO en TIME STATISTICS kan de impact per query worden bepaald en worden bepaald of het gebruik van bitwise operators voordeliger is. Vergeet niet om in de testomgeving de cache en buffers leeg te maken voor elke test, zodat de test zuiver wordt uitgevoerd.

Conclusie
Het gebruik van bitwise operators kan de doorlooptijd van query’s significant positief beïnvloeden en zijn daarom voor mij een welkome uitbreiding op mijn kennis (toolbox) bij query performance tuning.