I'd be surprised if this hasn't been asked before, but I haven't been able to find anything. Excel has a function
CHOOSE(n, x_1, x_2, x_3, ...)
which returns x_n for the given value of n.
Is there anything similar in SQL (either standard or MS-specific) supported by SQL Server 2008? I know it should really be implemented using a lookup table in the database, but for what I'm doing I'm not able to add new tables to the database.
I could create a temporary table and populate it from the SQL script, or use
CASE n WHEN 1 THEN x_1 WHEN 2 THEN x_2 WHEN 3 THEN x_3 ... END
but is there anything less cumbersome?
Unfortuantely, no it seems not to be the present in your version.
The CHOOSE-Function is only available since SQL Server 2012 and works quite the same as you describe the Excel-function.
"but for what I'm doing I'm not able to add new tables to the database". Well you always can use temporary table, table variable or, if it's really one time thing - derived table:
select
...,
l.v
from <your table> as t
left outer join (values
(1, x_1), (2, x_2), (3, x_3)
) as l(n, v) on l.n = t.n
Of course, you can always try to create your own choose() function:
create function dbo.f_Choose5(
#index int,
#value1 sql_variant,
#value2 sql_variant,
#value3 sql_variant,
#value4 sql_variant,
#value5 sql_variant
)
returns sql_variant
as
begin
return (
case #index
when 1 then #value1
when 2 then #value2
when 3 then #value3
when 4 then #value4
when 5 then #value5
end
)
end
select dbo.f_Choose5(3, 1, 2 ,3, 4, 5)
select dbo.f_Choose5(3, 1, 2 ,3, default, default)
but you have to keep in mind that scalar functions are not really optimized in SQL Server.
Related
I have a SQL Server function (see fn_TestBinaryBitwiseOr below) with millions of executions. While this is anticipated, SQL Server has an expensive query report that says:
There have been multiple executions of this query pattern (query
hash), but all of the executions are generating the same query plan
(query plan hash). This wastes CPU time to compile the query plans
and memory to cache them. To improve overall SQL Server performance,
consider parameterizing this query (via the application, template plan
guide, or forced parameterization). Refer to Books Online for more
information on parameterization.
I am pretty sure I am hitting RedFlag #1 from https://eitanblumin.com/2022/08/25/too-many-plans-for-the-same-query-hash/ but still not sure what I can do in my case. I cannot use a stored procedure because it is used in another function in a select and has the output. I haven't gone to forced parameterization due to some of the downsides but may look into that next.
Is it possible to change fn_TestBinaryBitwiseOr below to a parameterized procedure? Is there a different solution to test.
CREATE OR ALTER FUNCTION[dbo].[fn_TestBinaryBitwiseOr]
(#TestFlags1 binary(16),
#TestFlags2 binary(16))
RETURNS binary(16)
AS
BEGIN
RETURN (SELECT TOP(1)
CASE
WHEN #TestFlags1 IS NULL AND #TestFlags2 IS NULL
THEN 0x0
WHEN #TestFlags1 IS NULL
THEN #TestFlags2
WHEN #TestFlags2 IS NULL
THEN #TestFlags1
ELSE
CONVERT(binary(16), CONVERT(binary(4), (SUBSTRING(#TestFlags2, 1, 4) | CONVERT(bigint, SUBSTRING(#TestFlags1, 1, 4)))) +
CONVERT(binary(4), (SUBSTRING(#TestFlags2, 5, 4) | CONVERT(bigint, SUBSTRING(#TestFlags1, 5, 4)))) +
CONVERT(binary(4), (SUBSTRING(#TestFlags2, 9, 4) | CONVERT(bigint, SUBSTRING(#TestFlags1,9,4)))) +
CONVERT(binary(4), (SUBSTRING(#TestFlags2, 13, 4) | CONVERT(bigint, SUBSTRING(#TestFlags1, 13, 4))))
)
END
)
END
GO
-- another function to call my first function,
CREATE OR ALTER FUNCTION[dbo].[DoAThing]()
RETURNS #T TABLE(ColName binary(16))
AS
BEGIN
-- sample data
DECLARE #Table1 TABLE (id int, binary1 binary(16))
DECLARE #Table2 TABLE(table1Id int, binary2 binary(16))
INSERT INTO #Table1
VALUES (1, 0x10000000000000000000000000000A01),
(2, 0x00000000000000000000000000000101)
INSERT INTO #Table2
VALUES (1, 0x00000000000000000000000000000100),
(2, 0x00000000000000000000000000000100)
-- using a function from a function
INSERT INTO #T(ColName)
(SELECT [dbo].[fn_TestBinaryBitwiseOr](t1.binary1, t2.binary2) AS result
FROM #Table1 t1
JOIN #Table2 t2 ON t1.id = t2.table1Id)
RETURN
END
GO
--executing
SELECT * FROM [dbo].[DoAThing]()
I am being passed the following parameter to my stored procedure -
#AddOns = 23:2,33:1,13:5
I need to split the string by the commas using this -
SET #Addons = #Addons + ','
set #pos = 0
set #len - 0
While CHARINDEX(',', #Addons, #pos+1)>0
Begin
SET #len = CHARINDEX(','), #Addons, #pos+1) - #pos
SET #value = SUBSTRING(#Addons, #pos, #len)
So now #value = 23:2 and I need to get 23 which is my ID and 2 which is my quantity. Here is the rest of my code -
INSERT INTO TABLE(ID, Qty)
VALUES(#ID, #QTY)
set #pos = CHARINDEX(',', #Addons, #pos+#len) + 1
END
So what is the best way to get the values of 23 and 2 in separate fields to us in the INSERT statement?
First you would split the sets of key-value pairs into rows (and it looks like you already got that far), and then you get the position of the colon and use that to do two SUBSTRING operations to split the key and value apart.
Also, this can be done much more efficiently than storing each row's key and value into separate variables just to get inserted into a table. If you INSERT from the SELECT that breaks this data apart, it will be a set-based operation instead of row-by-row.
For example:
DECLARE #AddOns VARCHAR(1000) = N'23:2,33:1,13:5,999:45';
;WITH pairs AS
(
SELECT [SplitVal] AS [Value], CHARINDEX(N':', [SplitVal]) AS [ColonIndex]
FROM SQL#.String_Split(#AddOns, N',', 1) -- https://SQLsharp.com/
)
SELECT *,
SUBSTRING(pairs.[Value], 1, pairs.[ColonIndex] - 1) AS [ID],
SUBSTRING(pairs.[Value], pairs.[ColonIndex] + 1, 1000) AS [QTY]
FROM pairs;
/*
Value ColonIndex ID QTY
23:2 3 23 2
33:1 3 33 1
13:5 3 13 5
999:45 4 999 45
*/
GO
For that example I am using a SQLCLR string splitter found in the SQL# library (that I am the author of), which is available in the Free version. You can use whatever splitter you like, including the built-in STRING_SPLIT that was introduced in SQL Server 2016.
It would be used as follows:
DECLARE #AddOns VARCHAR(1000) = N'23:2,33:1,13:5,999:45';
;WITH pairs AS
(
SELECT [value] AS [Value], CHARINDEX(N':', [value]) AS [ColonIndex]
FROM STRING_SPLIT(#AddOns, N',') -- built-in function starting in SQL Server 2016
)
INSERT INTO dbo.TableName (ID, QTY)
SELECT SUBSTRING(pairs.[Value], 1, pairs.[ColonIndex] - 1) AS [ID],
SUBSTRING(pairs.[Value], pairs.[ColonIndex] + 1, 1000) AS [QTY]
FROM pairs;
Of course, the Full (i.e. paid) version of SQL# includes an additional splitter designed to handle key-value pairs. It's called String_SplitKeyValuePairs and works as follows:
DECLARE #AddOns VARCHAR(1000) = N'23:2,33:1,13:5,999:45';
SELECT *
FROM SQL#.String_SplitKeyValuePairs(#AddOns, N',', N':', 1, NULL, NULL, NULL);
/*
KeyID Key Value
1 23 2
2 33 1
3 13 5
4 999 45
*/
GO
So, it would be used as follows:
DECLARE #AddOns VARCHAR(1000) = N'23:2,33:1,13:5,999:45';
INSERT INTO dbo.[TableName] ([Key], [Value])
SELECT kvp.[Key], kvp.[Value]
FROM SQL#.String_SplitKeyValuePairs(#AddOns, N',', N':', 1, NULL, NULL, NULL) kvp;
Check out this blog post...
http://www.sqlservercentral.com/blogs/querying-microsoft-sql-server/2013/09/19/how-to-split-a-string-by-delimited-char-in-sql-server/
Noel
I am going to make another attempt at this inspired by the answer given by #gofr1 on this question...
How to insert bulk of column data to temp table?
That answer showed how to use an XML variable and the nodes method to split comma separated data and insert it into individual columns in a table. It seemed to me to be very similar to what you were trying to do here.
Check out this SQL. It certainly isn't has concise as just having "split" function, but it seems better than chopping up the string based on position of the colon.
Noel
I have a Table with a computed column that uses a scalar function
CREATE FUNCTION [dbo].[ConvertToMillimetres]
(
#fromUnitOfMeasure int,
#value decimal(18,4)
)
RETURNS decimal(18,2)
WITH SCHEMABINDING
AS
BEGIN
RETURN
CASE
WHEN #fromUnitOfMeasure=1 THEN #value
WHEN #fromUnitOfMeasure=2 THEN #value * 100
WHEN #fromUnitOfMeasure=3 THEN #value * 1000
WHEN #fromUnitOfMeasure=4 THEN #value * 25.4
ELSE #value
END
END
GO
The table has this column
[LengthInMm] AS (CONVERT([decimal](18,2),[dbo].[ConvertToMillimetres]([LengthUnitOfMeasure],[Length]))) PERSISTED,
Assuming that the [Length] on the Table is 62.01249 and [LengthUnitOfMeasure] is 4 the LengthInMm computed value comes with 1575.11 but when i run the function directly like
SELECT [dbo].[ConvertToMillimetres] (4, 62.01249) GO
It comes with 1575.12
[Length] column is (decimal(18,4, null))
Can anyone tell why this happens?
I'm not sure this really counts as an answer, but you've perhaps got a problem with a specific version of SQL?
I just had a go at replicating it (on a local SQL 2014 install) and got the following:
create table dbo.Widgets (
Name varchar(20),
Length decimal(18,4),
LengthInMm AS [dbo].[ConvertToMillimetres] (4, Length) PERSISTED
)
insert into dbo.Widgets (Name, Length) values ('Thingy', 62.01249)
select * from dbo.Widgets
Which gives the result:
Name Length LengthInMm
-------------------- --------------------------------------- ---------------------------------------
Thingy 62.0125 1575.12
(1 row(s) affected)
Note that your definition uses [LengthInMm] AS (CONVERT([decimal](18,2),[dbo].[ConvertToMillimetres]([LengthUnitOfMeasure],[Length]))) PERSISTED, but that doesn't seem to make a difference to the result.
I also tried on my PC (Microsoft SQL Server 2012 - 11.0.2100.60 (X64)). Works fine:
CREATE TABLE dbo.data(
LengthUnitOfMeasure INT,
[Length] decimal(18,4),
[LengthInMm] AS (CONVERT([decimal](18,2),[dbo].[ConvertToMillimetres]([LengthUnitOfMeasure],[Length]))) PERSISTED
)
INSERT INTO dbo.data (LengthUnitOfMeasure, [Length])
SELECT 4, 62.01249
SELECT *
FROM dbo.data
/*
RESULT
LengthUnitOfMeasure Length LengthInMm
4 62.0125 1575.12
*/
I think, I found the answer:
Lets see what you are saying:
There is a column with decimal(18,4) data type.
There is a calculated column which depend on this column.
The result differs when you select the calculated field and when you provide the same result manually. (Right?)
Sorry, but the input parameters are not the same:
The column in the table is decimal(18,4). The value you are provided manually is decimal(7,5) (62.01249)
Since the column in the table can not store any values with scale of 5, the provided values will not be equal. (Furthermore there is no record in the table with the value of 62.01249 in the Length column)
What is the output when you query the [Length] column from the table? Is it 62.0124? If yes, then this is the answer. The results can not be equal since the input values are not equal.
To be a bit more specific: 62.01249 will be casted (implicit cast) to 62.0125.
ROUND(25.4 * 62.0124, 2) = 1575.11
ROUND(25.4 * 62.0125, 2) = 1575.12
EDIT Everybody who tried to rebuild the schema made the same mistake (Including me). When we (blindly) inserted the values from the original question into our instances, we inserted the 62.01249 into the Length column -> the same implicit cast occured, so we have the value 62.0125 in our tables.
I have a periodic check of a certain query (which by the way includes multiple tables) to add informational messages to the user if something has changed since the last check (once a day).
I tried to make it work with checksum_agg(binary_checksum(*)), but it does not help, so this question doesn't help much, because I have a following case (oversimplified):
select checksum_agg(binary_checksum(*))
from
(
select 1 as id,
1 as status
union all
select 2 as id,
0 as status
) data
and
select checksum_agg(binary_checksum(*))
from
(
select 1 as id,
0 as status
union all
select 2 as id,
1 as status
) data
Both of the above cases result in the same check-sum, 49, and it is clear that the data has been changed.
This doesn't have to be a simple function or a simple solution, but I need some way to uniquely identify the difference like these in SQL server 2000.
checksum_agg appears to simply add the results of binary_checksum together for all rows. Although each row has changed, the sum of the two checksums has not (i.e. 17+32 = 16+33). This is not really the norm for checking for updates, but the recommendations I can come up with are as follows:
Instead of using checksum_agg, concatenate the checksums into a delimited string, and compare strings, along the lines of SELECT binary_checksum(*) + ',' FROM MyTable FOR XML PATH(''). Much longer string to check and to store, but there will be much less chance of a false positive comparison.
Instead of using the built-in checksum routine, use HASHBYTES to calculate MD5 checksums in 8000 byte blocks, and xor the results together. This will give you a much more resilient checksum, although still not bullet-proof (i.e. it is still possible to get false matches, but very much less likely). I'll paste the HASHBYTES demo code that I wrote below.
The last option, and absolute last resort, is to actually store the table table in XML format, and compare that. This is really the only way you can be absolutely certain of no false matches, but is not scalable and involves storing and comparing large amounts of data.
Every approach, including the one you started with, has pros and cons, with varying degrees of data size and processing requirements against accuracy. Depending on what level of accuracy you require, use the appropriate option. The only way to get 100% accuracy is to store all of the table data.
Alternatively, you can add a date_modified field to each table, which is set to GetDate() using after insert and update triggers. You can do SELECT COUNT(*) FROM #test WHERE date_modified > #date_last_checked. This is a more common way of checking for updates. The downside of this one is that deletions cannot be tracked.
Another approach is to create a modified table, with table_name (VARCHAR) and is_modified (BIT) fields, containing one row for each table you wish to track. Using insert, update and delete triggers, the flag against the relevant table is set to True. When you run your schedule, you check and reset the is_modified flag (in the same transaction) - along the lines of SELECT #is_modified = is_modified, is_modified = 0 FROM tblModified
The following script generates three result sets, each corresponding with the numbered list earlier in this response. I have commented which output correspond with which option, just before the SELECT statement. To see how the output was derived, you can work backwards through the code.
-- Create the test table and populate it
CREATE TABLE #Test (
f1 INT,
f2 INT
)
INSERT INTO #Test VALUES(1, 1)
INSERT INTO #Test VALUES(2, 0)
INSERT INTO #Test VALUES(2, 1)
/*******************
OPTION 1
*******************/
SELECT CAST(binary_checksum(*) AS VARCHAR) + ',' FROM #test FOR XML PATH('')
-- Declaration: Input and output MD5 checksums (#in and #out), input string (#input), and counter (#i)
DECLARE #in VARBINARY(16), #out VARBINARY(16), #input VARCHAR(MAX), #i INT
-- Initialize #input string as the XML dump of the table
-- Use this as your comparison string if you choose to not use the MD5 checksum
SET #input = (SELECT * FROM #Test FOR XML RAW)
/*******************
OPTION 3
*******************/
SELECT #input
-- Initialise counter and output MD5.
SET #i = 1
SET #out = 0x00000000000000000000000000000000
WHILE #i <= LEN(#input)
BEGIN
-- calculate MD5 for this batch
SET #in = HASHBYTES('MD5', SUBSTRING(#input, #i, CASE WHEN LEN(#input) - #i > 8000 THEN 8000 ELSE LEN(#input) - #i END))
-- xor the results with the output
SET #out = CAST(CAST(SUBSTRING(#in, 1, 4) AS INT) ^ CAST(SUBSTRING(#out, 1, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 5, 4) AS INT) ^ CAST(SUBSTRING(#out, 5, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 9, 4) AS INT) ^ CAST(SUBSTRING(#out, 9, 4) AS INT) AS VARBINARY(4)) +
CAST(CAST(SUBSTRING(#in, 13, 4) AS INT) ^ CAST(SUBSTRING(#out, 13, 4) AS INT) AS VARBINARY(4))
SET #i = #i + 8000
END
/*******************
OPTION 2
*******************/
SELECT #out
I am trying to create T-SQL function from Northwind to return new table, that will containt ProductID, ProductName, UnitsInStock and new column indicating if there are more UnitsInStock than function parameter.
Example: Let's have table of 2 products. First has 10 units in stock, second has 5. So function with parameter 6 should return:
1, Product1, 10, YES
2, Product2, 5, NO
Here's my non working code sofar :(
CREATE FUNCTION dbo.ProductsReorder
(
#minValue int
)
RETURNS #tabvar TABLE (int _ProductID, nvarchar _ProductName, int _UnitsInStock, nvarchar _Reorder)
AS
BEGIN
INSERT INTO #tabvar
SELECT ProductID, ProductName, UnitsInStock, Reorder =
CASE
WHEN UnitsInStock > #minValue THEN "YES"
ELSE "NO"
END
FROM Products
RETURN
END
T-SQL gives me this not really helpful answer: "Column, parameter, or variable#1: Cannot find data type _ProductID". I googled but I found gazillion different issues for such a result.
I dunno if it's good to use CASE here, I have a little Oracle background and decode function was great for these issues.
it's an easy answer -- especially as you are from oracle
the table definition in your function is the wrong way round.
Replace:
#tabvar TABLE (int _ProductID, nvarchar _ProductName, int _UnitsInStock, nvarchar _Reorder)
with something like
#tabvar TABLE ([_ProductID] INT, [_ProductName] NVARCHAR(50), [_UnitsInStrock] INT, [_Reorder] NVARCHAR(50))
In sql server the types come after the column names
In your table definition, you should put the column name first, then the data type. For example
_UnitsInStock int, ...
also, the NVARCHAR data type needs a length value.
_ProductName nvarchar(20)