Logic IF someone is born on the 21 century sql server - sql

I've built a function that calculates age, all personal ID's I get are 10 numbers according to swedish ssn which the format is 650101-1234. So far in my function it does calculate age, but only for people born from 1900 to 1999 How would I go about adding the posibility to calcuate the age on persons born from 2000 and forward
Here is my code for the function.
ALTER function [dbo].[Function_AGE]
(
#IdNr varchar(13)
)
returns int
as
begin
Declare #Calc int
Set #IdNr = (Select Case when LEN(#IdNr) > 11
then cast(left(#IdNr, 8) as date)
else cast('19'+left(#IdNr,6) as date) end)
set #Calc = (select datediff(year,#IdNr,getdate()))
Return #Calc
end

If the first two digits fall between 00 and 14 and the character in the seventh position isn't a plus sign the person is born after 1999. So you can add if-then-else logic to test for that.
Here's a not too tested example on how to determine brackets, it's far from perfect and is only meant to serve as a hint - and it probably contains errors :)
declare #people table (pnr char(11))
insert #people values ('121212-1212'),('121212+1212'),('991212-1212')
select pnr,
case
when substring(pnr,7,1)='-'
and left(pnr, 2) between 00 and left(year(getdate()), 2)
then 'young (after 2000)'
when substring(pnr,7,1)='+'
and left(pnr, 2) between 00 and left(year(getdate()), 2)
then 'old 100+ (before 1914)'
when substring(pnr,7,1)='-'
and not left(pnr, 2) between 00 and left(year(getdate()), 2)
then 'normal (between 1914 and 1999)'
end as [Age span]
from #people
Result:
pnr Age span
----------- ------------------------------
121212-1212 young (after 2000)
121212+1212 old 100+ (before 1914)
991212-1212 normal (between 1914 and 1999)

Here is my answer, I've tested it and it works perfectly! Thank you for the tips I've got it helped me along the way. Much appreciated.
ALTER function [dbo].[AGE_Function]
(
#IdNr varchar(13)
)
returns int
as
begin
If SUBSTRING(#IdNr,1,2) between '20'+'00' and DATEPART(YY,GETDATE())
and LEFT(#IdNr,7) = '-'
Begin
Declare #Calc int
Set #IdNr = (Select Case when LEN(#IdNr) > 11
then cast(left(#IdNr, 8) as date)
else cast('20'+left(#IdNr,6) as date) end)
set #Calc = (select datediff(year,#IdNr,getdate()))
end
Else
Begin
Set #IdNr = (Select Case when LEN(#IdNr) > 11
then cast(left(#IdNr, 8) as date)
else cast('19'+left(#IdNr,6) as date) end)
set #Calc = (select datediff(year,#IdNr,getdate()))
end
Return #Calc
end

Related

SQL Server : problem updating a counter type column in a table. All rows get the same value and they should not

I'm new to SQL and looking for assistance updating every record in a table column with a calculated value. I'm sure I am overthinking this!
I need to loop through all the records (870K+) and increment a counter variable with a value between 01 and 99 (2 digits) which will replace the value in the MemoKeyExtension (NChar 2) column in my table.
To keep it simple, you can assume this table just has a uniqueid column, an account number column (int), and the MemoKeyExtension (nChar2) column.
DECLARE #COUNT as Int
SET #COUNT = 0
DECLARE Mem_Cursor CURSOR FOR
SELECT MemoKeyExtension FROM Customer.Memo
OPEN MEM_Cursor
FETCH NEXT FROM MEM_Cursor
WHILE ##FETCH_STATUS = 0
BEGIN
IF #COUNT < 99
BEGIN
SET #COUNT = #COUNT + 1
END
ELSE
BEGIN
SET #COUNT = 1
END
UPDATE Customer.Memo
SET MemoKeyExtension = REPLACE(STR(#COUNT,2),' ','0')
WHERE CURRENT OF MEM_Cursor
FETCH NEXT FROM MEM_Cursor
END
CLOSE MEM_Cursor
DEALLOCATE MEM_Cursor
Example of desired results:
MemoId MemoKeyExtension
F630A22A-6BAA-4F86-84FB-0823FCAD95A0 95
7BE0E85D-9CA8-4E01-888E-781D5356EDB8 96
5D7BD8F2-5858-4A8F-96B0-E93D44030925 97
9520A24A-5168-41E8-ADFF-429513EB3693 98
5890818B-6EBB-4CD4-94C6-D467679427E5 99
B730FD54-F231-494E-A7F0-E7162F2EDD10 01
578A2457-39DF-41A4-953C-4A3C9E69F394 02
F316C39C-6ACA-4A7C-A401-39518F2EF9A1 03
Thank you for any help you can provide.
Mostly guessing here since we have no table definitions to work with. If this doesn't work it should be really close.
with MyNumbers as
(
select NewVal = right('0' + convert(varchar(2), ROW_NUMBER() over (order by (select null)) % 99), 2)
, MemoId
from Customer.Memo m
)
update m
set MemoKeyExtension = case when NewVal = '00' then '99' else n.NewVal end
from Customer.Memo m
join MyNumbers n on n.MemoId = m.MemoId

How to improve while loop insert performance in sql server?

Here is my SQL Query. It's insert almost 6500+ row from temp table. But its takes 15+ mins! . How can i improve this ? Thanks
ALTER proc [dbo].[Process_bill]
#userid varchar(10),
#remark nvarchar(500),
#tdate date ,
#pdate date
as
BEGIN
IF OBJECT_ID('tempdb.dbo..#temptbl_bill', 'U') IS NOT NULL
DROP TABLE #temptbl_bill;
CREATE TABLE #temptbl_bill (
RowID int IDENTITY(1, 1),
------------
)
// instert into temp table
DECLARE #NumberRecords int, #RowCounter int
DECLARE #batch INT
SET #batch = 300
SET #NumberRecords = (SELECT COUNT(*) FROM #temptbl_bill)
SET #RowCounter = 1
SET NOCOUNT ON
BEGIN TRANSACTION
WHILE #RowCounter <= #NumberRecords
BEGIN
declare #clid int
declare #hlid int
declare #holdinNo nvarchar(150)
declare #clientid nvarchar(100)
declare #clientName nvarchar(50)
declare #floor int
declare #radius nvarchar(50)
declare #bill money
declare #others money
declare #frate int
declare #due money
DECLARE #fine money
DECLARE #rebate money
IF #RowCounter > 0 AND ((#RowCounter % #batch = 0) OR (#RowCounter = #NumberRecords))
BEGIN
COMMIT TRANSACTION
PRINT CONCAT('Transaction #', CEILING(#RowCounter/ CAST(#batch AS FLOAT)), ' committed (', #RowCounter,' rows)');
BEGIN TRANSACTION
END;
// multiple select
// insert to destination table
Print 'RowCount -' +cast(#RowCounter as varchar(20)) + 'batch -' + cast(#batch as varchar(20))
SET #RowCounter = #RowCounter + 1;
END
COMMIT TRANSACTION
PRINT CONCAT('Transaction #', CEILING(#RowCounter/ CAST(#batch AS FLOAT)), ' committed (',
#RowCounter,' rows)');
SET NOCOUNT OFF
DROP TABLE #temptbl_bill
END
GO
As has been said in comments, the loop is completely unnecessary. The way to improve the performance of any loop is to remove it completely. Loops are a last resort in SQL.
As far as I can tell your insert can be written with a single statement:
INSERT tbl_bill(clid, hlid, holdingNo,ClientID, ClientName, billno, date_month, unit, others, fine, due, bill, rebate, remark, payment_date, inserted_by, inserted_date)
SELECT clid = c.id,
hlid = h.id,
h.holdinNo ,
c.cliendID,
clientName = CAST(c.clientName AS NVARCHAR(50)),
BillNo = CONCAT(h.holdinNo, MONTH(#tdate), YEAR(#tdate)),
date_month = #tDate,
unit = 0,
others = CASE WHEN h.hfloor = 0 THEN rs.frate * (h.hfloor - 1) ELSE 0 END,
fine = bs.FineRate * b.Due / 100,
due = b.Due,
bill = #bill, -- This is declared but never assigned
rebate = bs.rebate,
remark = #remark,
payment_date = #pdate,
inserted_by = #userid,
inserted_date = GETDATE()
FROM ( SELECT id, clientdID, ClientName
FROM tbl_client
WHERE status = 1
) AS c
INNER JOIN
( SELECT id, holdinNo, [floor], connect_radius
FROM tx_holding
WHERE status = 1
AND connect_radius <> '0'
AND type = 'Residential'
) AS h
ON c.id = h.clid
LEFT JOIN tbl_radius_setting AS rs
ON rs.radius= CONVERT(real,h.connect_radius)
AND rs.status = 1
AND rs.type = 'Non-Govt.'
LEFT JOIN tbl_bill_setting AS bs
ON bs.Status = 1
LEFT JOIN
( SELECT hlid,
SUM(netbill) AS Due
FROM tbl_bill AS b
WHERE date_month < #tdate
AND (b.ispay = 0 OR b.ispay IS NULL)
GROUP BY hlid
) AS b
ON b.hlid = h.id
WHERE NOT EXISTS
( SELECT 1
FROM tbl_bill AS tb
WHERE EOMONTH(#tdate) = EOMONTH(date_month)
AND tb.holdingNo = h.holdinNo
AND (tb.update_by IS NOT NULL OR tb.ispay=1)
);
Please take this with a pinch of salt, it was quite hard work trying to piece together the logic, so it may need some minor tweaks and corrections
As well as adapting this to work as a single statement, I have made a number of modifications to your existing code:
Swapped NOT IN for NOT EXISTS to avoid any issues with null records. If holdingNo is nullable, they are equivalent, if holdingNo is nullable, NOT EXISTS is safer - Not Exists Vs Not IN
The join syntax you are using was replaced 27 years ago, so I switched from ANSI-89 join syntax to ANSI-92. - Bad habits to kick : using old-style JOINs
Changed predicates of YEAR(date_month) = YEAR(#tDate) AND MONTH(date_month) = MONTH(#tDate) to become EOMONTH(#tdate) = EOMONTH(date_month). These are syntactically the same, but EOMONTH is Sargable, whereas MONTH and YEAR are not.
Then a few further links/suggestions that are directly related to changes I have made
Although I removed the while lopp, don't fall into the trap of thinking this is better than a cursor. A properly declared cursor will out perform a while loop like yours - Bad Habits to Kick : Thinking a WHILE loop isn't a CURSOR
The general consensus is that prefixing object names is not a good idea. It should either be obvious from the context if an object is a table/view or function/procedure, or it should be irrelevant - i.e. There is no need to distinguish between a table or a view, and in fact, we may wish to change from one to the other, so having the prefix makes things worse, not better.
The average ratio of time spent reading code to time spent writing code is around 10:1 - It is therefore worth the effort to format your code when you are writing it so that it is easy to read. This is hugely subjective with SQL, and I would not recommend any particular conventions, but I cannot believe for a second you find your original code free flowing and easy to read. It took me about 10 minutes just unravel the first insert statement.
EDIT
The above is not correct, EOMONTH() is not sargable, so does not perform any better than YEAR(x) = YEAR(y) AND MONTH(x) = MONTH(y), although it is still a bit simpler. If you want a truly sargable predicate you will need to create a start and end date using #tdate, so you can use:
DATEADD(MONTH, DATEDIFF(MONTH, '19000101', #tdate), '19000101')
to get the first day of the month for #tdate, then almost the same forumla, but add months to 1st February 1900 rather than 1st January to get the start of the next month:
DATEADD(MONTH, DATEDIFF(MONTH, '19000201', #tdate), '19000201')
So the following:
DECLARE #Tdate DATE = '2019-10-11';
SELECT DATEADD(MONTH, DATEDIFF(MONTH, '19000101', #tdate), '19000101'),
DATEADD(MONTH, DATEDIFF(MONTH, '19000201', #tdate), '19000201');
Will return 1st October and 1st November respectively. Putting this back in your original query would give:
WHERE NOT EXISTS
( SELECT 1
FROM tbl_bill AS tb
WHERE date_month >= DATEADD(MONTH, DATEDIFF(MONTH, '19000101', #tdate), '19000101'),
AND date_month < DATEADD(MONTH, DATEDIFF(MONTH, '19000201', #tdate), '19000201')
AND tb.holdingNo = h.holdinNo
AND (tb.update_by IS NOT NULL OR tb.ispay=1)
);

Cannot insert the value NULL into column with procedure

This is not a duplicate.
I do understand what the issue means but I don't understand why because the variable contains data. I'm basically trying to make a char(4) column increase alone (just like identity with integers). If the table doesn't contain anything, the first value would be 'C001' otherwise, It simply increase based on the last record.
CREATE PROCEDURE ADD_CL(#nom VARCHAR(20),
#dn DATE)
AS
BEGIN
DECLARE #B CHAR(4)
DECLARE #B_to_int INT
DECLARE #B_new_value CHAR(4)
IF EXISTS(SELECT TOP 1 *
FROM CLIENT)
SET #B_new_value = 'C001'
ELSE
BEGIN
SELECT TOP 1 #B = code_client
FROM client
ORDER BY code_client DESC
SET #B_to_int = CAST(SUBSTRING(#B, 2, 3) AS INTEGER)
SET #B_to_int = #B_to_int + 1;
SET #B_new_value = LEFT(#B, 1) + RIGHT('00' + CAST(#B_to_int AS INT), 3)
END
INSERT INTO CLIENT
VALUES (#B_new_value,
#nom,
#dn)
END
Cannot insert the value NULL into column 'code_client', table 'dbo.CLIENT'; column does not allow nulls. INSERT fails.
#B_new_value represent code_client
Your If Exists should be If Not Exists.
So change
if exists(select TOP 1 * from CLIENT)
to
if not exists(select TOP 1 * from CLIENT)
Also you are adding 00 to your final #B_to_int which is cast as int. so it will show C2,C3 and so on.
If you want to retain the same format, cast it to varchar
SET #B_new_value = LEFT(#B,1) + '00' + CAST(#B_to_int as varchar)
Above line will work only till the count is 9. and then it will continue replicating itself with 1 because 10 will be 0010 and final output will be C0010. To eliminate this issue, use replicate and replicate 0 until 3 characters.
SET #B_new_value = LEFT(#B,1) + REPLICATE('0',3-LEN(#B_to_int)) + #B_to_int
Good Luck.
The other answers already tell you that you should be using NOT EXISTS.
This numbering scheme is quite possibly something you'll regret but you could simplify this a lot as well as making it safer in conditions of concurrency and when you run out of numbers by just doing
CREATE PROCEDURE ADD_CL(#nom VARCHAR(20),
#dn DATE)
AS
BEGIN
DECLARE #B VARCHAR(5);
SET XACT_ABORT ON;
BEGIN TRAN
SELECT #B = FORMAT(1 + RIGHT(ISNULL(MAX(code_client), 'C000'), 3), '\C000')
FROM CLIENT WITH(ROWLOCK, UPDLOCK, HOLDLOCK);
IF ( LEN(#B) > 4 )
THROW 50000, 'Exceeded range',1;
INSERT INTO CLIENT
VALUES (#B,
#nom,
#dn);
COMMIT
END
I believe the following should be 'NOT EXISTS'
if EXISTS(select TOP 1 * from CLIENT)

Combining SUM and CASE returns 0?

Against SQL Server, I'm essentially trying to calculate a value based on Year to Date, so I want to sum any values from July 16, 2012 and prior and display them. I'm using the following query (note that I've replaced parameters with simple integers to calculate the value for today):
SELECT SUM(CASE
WHEN (
(
dns.ODAY <= 16
AND (dns.fiscalyear + 1) = 13
AND dns.omonth = 7
)
OR
(
(dns.fiscalyear + 1) = 13
AND dns.omonth < 7
)
)
THEN dns.QtyShipped
ELSE 0
END) AS Shipped_Units
FROM myTable dns
However, this query is returning 0 for all rows. If I replace dns.QtyShipped with an integer, say 1, it still returns 0. So obviously the case statement isn't being evaluated correctly. Is my logic flawed? Or is it a syntax issue (e.g. I need more parentheses)?
Thanks!
Additional comments:
To test, I've ran the following query:
SELECT SUM(dns.QtyShipped)
FROM myTable dns
where
(dns.ODAY <= 16
AND (dns.fiscalyear + 1) = 13
AND dns.omonth = 7)
OR
((dns.fiscalyear + 1) = 13
AND dns.omonth < 7)
Which returns a very large number. This is confusing.
The code that you mentioned earlier is working absolutely fine. Please double check the values you are using to evaluate the conditions. For example, please confirm if for fiscalyear the value is 2013 or 13. I've used variables instead of column names in the code mentioned below and its returning the expected results:
declare #ODAY integer
set #ODAY=17
declare #fiscalyear int
set #fiscalyear=12
declare #omonth int
set #omonth=8
SELECT SUM(CASE
WHEN (
(
#ODAY <= 16
AND (#fiscalyear + 1) = 13
AND #omonth = 7
)
OR
(
(#fiscalyear + 1) = 13
AND #omonth < 7
)
)
THEN 1
ELSE 0
END) AS Shipped_Units
If I had to guess I would say that your year is being stored as 4 digits. At least that is the problem I ran into when I set up my test.
When I set up this test it worked:
CREATE TABLE myTable (fiscalyear int, omonth int, ODAY int, qtyshipped int)
INSERT INTO myTable VALUES (2012,1,1,1),
(12,1,1,1),
(12,2,1,1),
(12,3,1,1),
(12,4,1,1),
(13,1,1,1),
(12,7,1,1)
When I set up this test it failed:
CREATE TABLE myTable (fiscalyear int, omonth int, ODAY int, qtyshipped int)
INSERT INTO myTable VALUES (2012,1,1,1),
(2012,1,1,1),
(2012,2,1,1),
(2012,3,1,1),
(2012,4,1,1),
(2013,1,1,1),
(2012,7,1,1)
Is there any reason you aren't using actual dates? Your logic would be much simpler and if the dates are stored in your table then the query would probably be faster too.
EDIT: Here is an additional test you can run to be sure its your case causing the problem:
SELECT SUM(CASE
WHEN (
(
dns.ODAY <= 16
AND (dns.fiscalyear + 1) = 13
AND dns.omonth = 7
)
OR
(
(dns.fiscalyear + 1) = 13
AND dns.omonth < 7
)
)
THEN 0
ELSE dns.QtyShipped
END) AS Shipped_Units
FROM myTable dns
Basically flip the case around. Return 0 if you are true and the QtyShipped if not. If you get a value this way then the problem is in your case, if you don't then the problem is probably somewhere else in your query.

USPS ACS Keyline Check Digit

I have implemented the "MOD 10" check digit algorithm using SQL, for the US Postal Service Address Change Service Keyline according to the method in their document, but it seems I'm getting the wrong numbers! Our input strings have only numbers in them, making the calculation a little easier. When I compare my results with the results from their testing application, I get different numbers. I don't understand what is going on? Does anyone see anything wrong with my algorithm? It's got to be something obvious...
The documentation for the method can be found on page 12-13 of this document:
http://www.usps.com/cpim/ftp/pubs/pub8a.pdf
The sample application can be found at:
http://ribbs.usps.gov/acs/documents/tech_guides/KEYLINE.EXE
PLEASE NOTE: I fixed the code below, based on the help from forum users. This is so that future readers will be able to use the code in its entirety.
ALTER function [dbo].[udf_create_acs] (#MasterCustomerId varchar(26))
returns varchar(30)
as
begin
--this implements the "mod 10" check digit calculation
--for the US Postal Service ACS function, from "Publication 8A"
--found at "http://www.usps.com/cpim/ftp/pubs/pub8a.pdf"
declare #result varchar(30)
declare #current_char int
declare #char_positions_odd varchar(10)
declare #char_positions_even varchar(10)
declare #total_value int
declare #check_digit varchar(1)
--These strings represent the pre-calculated values of each character
--Example: '7' in an odd position in the input becomes 14, which is 1+4=5
-- so the '7' is in position 5 in the string - zero-indexed
set #char_positions_odd = '0516273849'
set #char_positions_even = '0123456789'
set #total_value = 0
set #current_char = 1
--stepping through the string one character at a time
while (#current_char <= len(#MasterCustomerId)) begin
--this is the calculation for the character's weighted value
if (#current_char % 2 = 0) begin
--it is an even position, so just add the digit's value
set #total_value = #total_value + convert(int, substring(#MasterCustomerId, #current_char, 1))
end else begin
--it is an odd position, so add the pre-calculated value for the digit
set #total_value = #total_value + (charindex(substring(#MasterCustomerId, #current_char, 1), #char_positions_odd) - 1)
end
set #current_char = #current_char + 1
end
--find the check digit (character) using the formula in the USPS document
set #check_digit = convert(varchar,(10 - (#total_value % 10)) % 10)
set #result = '#' + #MasterCustomerId + ' ' + #check_digit + '#'
return #result
end
I'm not sure why you're messing with the whole string representations when you're working in a set-based language.
I'd probably do it like below. I ran four tests through and they were all successful. You can expand this easily to handle characters as well and you could even make the table permanent if you really wanted to do that.
CREATE FUNCTION dbo.Get_Mod10
(
#original_string VARCHAR(26)
)
RETURNS VARCHAR(30)
AS
BEGIN
DECLARE
#value_mapping TABLE (original_char CHAR(1) NOT NULL, odd_value TINYINT NOT NULL, even_value TINYINT NOT NULL)
INSERT INTO #value_mapping
(
original_char,
odd_value,
even_value
)
SELECT '0', 0, 0 UNION
SELECT '1', 2, 1 UNION
SELECT '2', 4, 2 UNION
SELECT '3', 6, 3 UNION
SELECT '4', 8, 4 UNION
SELECT '5', 1, 5 UNION
SELECT '6', 3, 6 UNION
SELECT '7', 5, 7 UNION
SELECT '8', 7, 8 UNION
SELECT '9', 9, 9
DECLARE
#i INT,
#clean_string VARCHAR(26),
#len_string TINYINT,
#sum SMALLINT
SET #clean_string = REPLACE(#original_string, ' ', '')
SET #len_string = LEN(#clean_string)
SET #i = 1
SET #sum = 0
WHILE (#i <= #len_string)
BEGIN
SELECT
#sum = #sum + CASE WHEN #i % 2 = 0 THEN even_value ELSE odd_value END
FROM
#value_mapping
WHERE
original_char = SUBSTRING(#clean_string, #i, 1)
SET #i = #i + 1
END
RETURN (10 - (#sum % 10)) % 10
END
GO
set #check_digit = convert(varchar, (10 - (#total_value % 10)) % 10)
Why do we have an additional mod:
convert(varchar, 10 % <<-- ?
The document says that only the last digit needs to be subtracted from 10. Did I miss anything?