Why does 9+1 = * , and how can I fix it? - sql-server

I'm having trouble with my database because its got auditing worked in and things are hard. I need to compare the current change with the last change so I thought I'd include a grouping column. But when I run my code, some of the column values are . It goes 1,2,3,4,5,6,7,8,9, . What the ... idon'tevenknow ?
This is what I'm doing:
DECLARE #Person char(11), #DonationYTD decimal(10, 2), #OldPerson char(11), #OldDonationYTD decimal(10, 2), #Group int;
SET #Group=1;
DECLARE TempCursor CURSOR FOR Select PersonID, DonationYTD FROM MyTable;
OPEN TempCursor;
FETCH NEXT FROM TempCursor INTO #Person, #DonationYTD ;
WHILE ##FETCH_STATUS=0
BEGIN
IF( #Person != #OldPerson)
SET #Group=1;
IF( #Person = #OldPerson AND #DonationYTD!=#OldDonationYTD)
SET #Group=#Group+1;
UPDATE MyTable SET CHANGEGROUP=#Group WHERE PersonID=#Person AND DonationYTD=#DonationYTD;
SET #OldPerson = #Person;
SET #OldDonationYTD = #DonationYTD;
FETCH NEXT FROM TempCursor INTO #Person, #DonationYTD ;
END
CLOSE TempCursor;
DEALLOCATE TempCursor;
SELECT PersonID, DonationYTD, Changegroup FROM MyTable
1 15.00 1
1 15.00 1
1 20.00 1
2 3.00 1
2 4.00 2
2 15.00 3
2 8.00 4
2 4.00 5
2 15.00 6
2 3.00 7
2 3.00 7
2 9.00 8
2 9.00 8
2 10.00 9
2 14.00 *
2 14.00 *
If I try to do anything with Changegroup it tells me it can't convert varchar symbol * to integer.
Why am I getting an asterisk? How can I fix it?

You are encountering the issue described here
When integers are implicitly converted to a character data type, if
the integer is too large to fit into the character field, SQL Server
enters ASCII character 42, the asterisk (*).
From which I deduce your column must be [var]char(1). This odd behaviour does not occur for the newer n[var]char types, as discussed here.
The fix would be to change the column datatype. Ideally to a numeric datatype such as int but if it must be string at least one long enough to hold the intended contents.

Related

Grouping between two datetimes

I have a bunch of production orders and I'm trying to group by within a datetime range, then count the quantity within that range. For example, I want to group from 2230 to 2230 each day.
PT.ActualFinish is datetime (eg. if PT.ActualFinish is 2020-05-25 23:52:30 then it would be counted on the 26th May instead of the 25th)
Currently it's grouped by date (midnight to midnight) as opposed to the desired 2230 to 2230.
GROUP BY CAST(PT.ActualFinish AS DATE)
I've been trying to reconcile some DATEADD with the GROUP without success. Is it possible?
Just add 1.5 hours (90 minutes) and then extract the date:
group by convert(date, dateadd(minute, 90, pt.acctualfinish))
For this kind of thing you can use a function I created called NGroupRangeAB (code below) which can be used to create groups over values with an upper and lower bound.
Note that this:
SELECT f.*
FROM core.NGroupRangeAB(0,1440,12) AS f
ORDER BY f.RN;
Returns:
RN GroupNumber Low High
--- ------------ ------ -------
0 1 0 120
1 2 121 240
2 3 241 360
3 4 361 480
4 5 481 600
5 6 601 720
6 7 721 840
7 8 841 960
8 9 961 1080
9 10 1081 1200
10 11 1201 1320
11 12 1321 1440
This:
SELECT
f.GroupNumber,
L = DATEADD(MINUTE,f.[Low]-SIGN(f.[Low]),CAST('00:00:00.0000000' AS TIME)),
H = DATEADD(MINUTE,f.[High]-1,CAST('00:00:00.0000000' AS TIME))
FROM core.NGroupRangeAB(0,1440,12) AS f
ORDER BY f.RN;
Returns:
GroupNumber L H
------------- ---------------- ----------------
1 00:00:00.0000000 01:59:00.0000000
2 02:00:00.0000000 03:59:00.0000000
3 04:00:00.0000000 05:59:00.0000000
4 06:00:00.0000000 07:59:00.0000000
5 08:00:00.0000000 09:59:00.0000000
6 10:00:00.0000000 11:59:00.0000000
7 12:00:00.0000000 13:59:00.0000000
8 14:00:00.0000000 15:59:00.0000000
9 16:00:00.0000000 17:59:00.0000000
10 18:00:00.0000000 19:59:00.0000000
11 20:00:00.0000000 21:59:00.0000000
12 22:00:00.0000000 23:59:00.0000000
Now for a real-life example that may help you:
-- Sample Date
DECLARE #table TABLE (tm TIME);
INSERT #table VALUES ('00:15'),('11:20'),('21:44'),('09:50'),('02:15'),('02:25'),
('02:31'),('23:31'),('23:54');
-- Solution:
SELECT
GroupNbr = f.GroupNumber,
TimeLow = f2.L,
TimeHigh = f2.H,
Total = COUNT(t.tm)
FROM core.NGroupRangeAB(0,1440,12) AS f
CROSS APPLY (VALUES(
DATEADD(MINUTE,f.[Low]-SIGN(f.[Low]),CAST('00:00:00.0000000' AS TIME)),
DATEADD(MINUTE,f.[High]-1,CAST('00:00:00.0000000' AS TIME)))) AS f2(L,H)
LEFT JOIN #table AS t
ON t.tm BETWEEN f2.L AND f2.H
GROUP BY f.GroupNumber, f2.L, f2.H;
Returns:
GroupNbr TimeLow TimeHigh Total
-------------------- ---------------- ---------------- -----------
1 00:00:00.0000000 01:59:00.0000000 1
2 02:00:00.0000000 03:59:00.0000000 3
3 04:00:00.0000000 05:59:00.0000000 0
4 06:00:00.0000000 07:59:00.0000000 0
5 08:00:00.0000000 09:59:00.0000000 1
6 10:00:00.0000000 11:59:00.0000000 1
7 12:00:00.0000000 13:59:00.0000000 0
8 14:00:00.0000000 15:59:00.0000000 0
9 16:00:00.0000000 17:59:00.0000000 0
10 18:00:00.0000000 19:59:00.0000000 0
11 20:00:00.0000000 21:59:00.0000000 1
12 22:00:00.0000000 23:59:00.0000000 2
Note that an inner join will eliminate the 0-count rows.
CREATE FUNCTION core.NGroupRangeAB
(
#min BIGINT, -- Group Number Lower boundary
#max BIGINT, -- Group Number Upper boundary
#groups BIGINT -- Number of groups required
)
/*****************************************************************************************
[Purpose]:
Creates an auxilliary table that allows for grouping based on a given set of rows (#rows)
and requested number of "row groups" (#groups). core.NGroupRangeAB can be thought of as a
set-based, T-SQL version of Oracle's WIDTH_BUCKET, which:
"...lets you construct equiwidth histograms, in which the histogram range is divided into
intervals that have identical size. (Compare with NTILE, which creates equiheight
histograms.)" https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions214.htm
See usage examples for more details.
[Author]:
Alan Burstein
[Compatibility]:
SQL Server 2008+
[Syntax]:
--===== Autonomous
SELECT ng.*
FROM dbo.NGroupRangeAB(#rows,#groups) AS ng;
[Parameters]:
#rows = BIGINT; the number of rows to be "tiled" (have group number assigned to it)
#groups = BIGINT; requested number of tile groups (same as the parameter passed to NTILE)
[Returns]:
Inline Table Valued Function returns:
GroupNumber = BIGINT; a row number beginning with 1 and ending with #rows
Members = BIGINT; Number of possible distinct members in the group
Low = BIGINT; the lower-bound range
High = BIGINT; the Upper-bound range
[Dependencies]:
core.rangeAB (iTVF)
[Developer Notes]:
1. An inline derived tally table using a CTE or subquery WILL NOT WORK. NTally requires
a correctly indexed tally table named dbo.tally; if you have or choose to use a
permanent tally table with a different name or in a different schema make sure to
change the DDL for this function accordingly. The recomended number of rows is
1,000,000; below is the recomended DDL for dbo.tally. Note the "Beginning" and "End"
of tally code.To learn more about tally tables see:
http://www.sqlservercentral.com/articles/T-SQL/62867/
2. For best results a P.O.C. index should exists on the table that you are "tiling". For
more information about P.O.C. indexes see:
http://sqlmag.com/sql-server-2012/sql-server-2012-how-write-t-sql-window-functions-part-3
3. NGroupRangeAB is deterministic; for more about deterministic and nondeterministic functions
see https://msdn.microsoft.com/en-us/library/ms178091.aspx
[Examples]:
-----------------------------------------------------------------------------------------
--===== 1. Basic illustration of the relationship between core.NGroupRangeAB and NTILE.
-- Consider this query which assigns 3 "tile groups" to 10 rows:
DECLARE #rows BIGINT = 7, #tiles BIGINT = 3;
SELECT t.N, t.TileGroup
FROM ( SELECT r.RN, NTILE(#tiles) OVER (ORDER BY r.RN)
FROM core.rangeAB(1,#rows,1,1) AS r) AS t(N,TileGroup);
Results:
N TileGroup
--- ----------
1 1
2 1
3 1
4 2
5 2
6 3
7 3
To pivot these "equiheight histograms" into "equiwidth histograms" we could do this:
DECLARE #rows BIGINT = 7, #tiles BIGINT = 3;
SELECT TileGroup = t.TileGroup,
[Low] = MIN(t.N),
[High] = MAX(t.N),
Members = COUNT(*)
FROM ( SELECT r.RN, NTILE(#tiles) OVER (ORDER BY r.RN)
FROM core.rangeAB(1,#rows,1,1) AS r) AS t(N,TileGroup);
GROUP BY t.TileGroup;
Results:
TileGroup Low High Members
---------- ---- ----- -----------
1 1 3 3
2 4 5 2
3 6 7 2
This will return the same thing at a tiny fraction of the cost:
SELECT TileGroup = ng.GroupNumber,
[Low] = ng.[Low],
[High] = ng.[High],
Members = ng.Members
FROM core.NGroupRangeAB(1,#rows,#tiles) AS ng;
--===== 2.1. Divide 25 Rows into 3 groups
DECLARE #min BIGINT = 1, #max BIGINT = 25, #groups BIGINT = 4;
SELECT ng.GroupNumber, ng.Members, ng.low, ng.high
FROM core.NGroupRangeAB(#min,#max,#groups) AS ng;
--===== 2.2. Assign group membership to another table
DECLARE #min BIGINT = 1, #max BIGINT = 25, #groups BIGINT = 4;
SELECT
ng.GroupNumber, ng.low, ng.high, s.WidgetId, s.Price
FROM (VALUES('a',$12),('b',$22),('c',$9),('d',$2)) AS s(WidgetId,Price)
JOIN core.NGroupRangeAB(#min,#max,#groups) AS ng
ON s.Price BETWEEN ng.[Low] AND ng.[High]
ORDER BY ng.RN;
Results:
GroupNumber low high WidgetId Price
------------ ---- ----- --------- ---------------------
1 1 7 d 2.00
2 8 13 a 12.00
2 8 13 c 9.00
4 20 25 b 22.00
-----------------------------------------------------------------------------------------
[Revision History]:
Rev 00 - 20190128 - Initial Creation; Final Tuning - Alan Burstein
****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT
RN = r.RN, -- Sort Key
GroupNumber = r.N2, -- Bucket (group) number
Members = g.S-ur.N+1, -- Count of members in this group
[Low] = r.RN*g.S+rc.N+ur.N, -- Lower boundary for the group (inclusive)
[High] = r.N2*g.S+rc.N -- Upper boundary for the group (inclusive)
FROM core.rangeAB(0,#groups-1,1,0) AS r -- Range Function
CROSS APPLY (VALUES((#max-#min)/#groups,(#max-#min)%#groups)) AS g(S,U) -- Size, Underflow
CROSS APPLY (VALUES(SIGN(SIGN(r.RN-g.U)-1)+1)) AS ur(N) -- get Underflow
CROSS APPLY (VALUES(#min+r.RN-(ur.N*(r.RN-g.U)))) AS rc(N); -- Running Count
GO

Why update with incremented variable creates duplicate values?

i'm using this query on SQLServer 2017:
DECLARE #id int
SET #id = 0
UPDATE aicidich00fuff
SET #id = trasco_id = #id + 1
but for some reason it doesn't create an unique value.
It is easily notable if I run the following:
select COUNT(*) cont, trasco_id from aicidich00fuff group by trasco_id order by trasco_id
cont trasco_id
4 1
4 2
4 3
4 4
4 5
4 6
4 7
4 8
4 9
and so on. Fun fact is that most values are repeated four times, then three times, then twice.
This query for autoincrementing a column has worked since a few weeks ago, but now every time i got this behaviour. Any tips?
Thanks

Convert Frequency Table Back to Non-Frequency Table (ungroup-ing)

In SQL Server, I have the following table (snippet) which is the source data I receive (I cannot get the raw table it was generated from).
Gradelevel | YoS | Inventory
4 | 0 | 4000
4 | 1 | 3500
4 | 2 | 2000
The first row of the table is saying for grade level 4, there are 4,000 people with 0 years of service (YoS).
I need to find the median YoS for each Grade level. This would be easy if the table wasn't given to me aggregated up to the Gradelevel/YoS level with a sum in the Inventory column, but sadly I'm not so lucky.
What I need is to ungroup this table such that I have a new table where the first record is in the table 4,000 times, the next record 3,500 times, the next 2,000, etc (the inventory column would not be in this new table). Then I could take the percent_disc() of the YoS column by grade level and get the median. I could also then use other statistical functions on YoS to glean other insights from the data.
So far I've looked at unpivot (doesn't appear to be a candidate for my use case), CTEs (can't find an example close to what I'm trying to do), and a function which iterates through the above table inserting the number of rows indicated by the value in inventory to a new table which becomes my 'ungrouped' table I can run statistical analyses on. I believe the last approach is the best option available to me but the examples I've all seen iterate and focus on a single column from a table. I need to iterate through each row, then use the gradelevel, and yos values to insert [inventory] number of times before moving on to the next row.
Is anyone aware of:
A better way to do this other then the iteration/cursor method?
How to iterate through a table to accomplish my goal? I've been reading Is there a way to loop through a table variable in TSQL without using a cursor? but am having a hard time figuring out how to apply that iteration to my use case.
Edit 10/3, here is the looping code I got working which produces the same as John's cross apply. Pro is any statistical function can then be run on it, con is it is slow.
--this table will hold our row (non-frequency) based inventory data
DROP TABLE IF EXISTS #tempinv
CREATE TABLE #tempinv(
amcosversionid INT NOT null,
pp NVARCHAR(3) NOT NULL,
gl INT NOT NULL,
yos INT NOT NULL
)
-- to transform the inventory frequency table to a row based inventory we need to iterate through it
DECLARE #MyCursor CURSOR, #pp AS NVARCHAR(3), #gl AS INT, #yos AS INT, #inv AS int
BEGIN
SET #MyCursor = CURSOR FOR
SELECT payplan, gradelevel, step_yos, SUM(inventory) AS inventory
FROM
mytable
GROUP BY payplan, gradelevel, step_yos
OPEN #MyCursor
FETCH NEXT FROM #MyCursor
INTO #pp, #GL, #yos, #inv
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #i int
SET #i = 1
--insert into our new table for each number of people in inventory
WHILE #i<=#inv
BEGIN
INSERT INTO #tempinv (pp,gl,yos) VALUES (#pp,#gl,#yos)
SET #i = #i + 1
END
FETCH NEXT FROM #MyCursor
INTO #pp, #GL, #yos, #inv
END;
One Option is to use an CROSS APPLY in concert with an ad-hoc tally table. This will "expand" your data into N rows. Then you can perform any desired analysis you want.
Example
Select *
From YourTable A
Cross Apply (
Select Top ([Inventory]) N=Row_Number() Over (Order By (Select NULL))
From master..spt_values n1, master..spt_values n2
) B
Returns
Grd Yos Inven N
4 0 4000 1
4 0 4000 2
4 0 4000 3
4 0 4000 4
4 0 4000 5
...
4 0 4000 3998
4 0 4000 3999
4 0 4000 4000
4 1 3500 1
4 1 3500 2
4 1 3500 3
4 1 3500 4
...
4 1 3500 3499
4 1 3500 3500
4 2 2000 1
4 2 2000 2
4 2 2000 3
...
4 2 2000 1999
4 2 2000 2000

A WHILE LOOP in SQL that I want to have loop once with 5 updates through all rows

I have a WHILE LOOP in an SQL query.
I have a table with 5 ROWS matching the counter
I'm randomizing 2048 rows and want to INSERT 1 - 5 over those rows, randomly into a single column but what I'm getting is, the query loops once over 2048 and inserts "1", then it loops a second time and inserts "5", then inserts, "3", then "4", and finally "2".
What I seek is loop through one time through the 2048 rows and insert randomly, 1 - 5 through 2048 rows (1 time) in the single column.
Here's the SQL which works but wrong.
declare #counter int
SET #counter = 1
BEGIN TRAN
WHILE (#counter <= 6)
BEGIN
SELECT id, city, wage_level
FROM myFirstTable
ORDER BY NEWID()
UPDATE myFirstTable
SET wage_level = #counter
SET #counter = #counter + 1
CONTINUE
END
COMMIT TRAN
The values in the table that contain 5 rows are irrelevant but fact that the "IDs" in that table are from 1 - 5 "ARE."
I'm close, but no cigar...
The result should be something like this:
id-----city------wage_level
---------------------
1 Denver 2
2 Chicago 3
3 Seattle 5
4 Los Angeles 1
5 Boise 4
---
2047 Charleston 2
2048 Rochester 1
And so on...
Thanks, everyone
No need for a loop. SQL works best on a set based approach.
Here is one way to do it:
Create and populate sample table (Please save us this step in your future questions)
CREATE TABLE myFirstTable
(
id int identity(1,1),
city varchar(20),
wage_level int
)
INSERT INTO myFirstTable (city) VALUES
('Denver'),
('Chicago'),
('Seattle'),
('Los Angeles'),
('Boise')
The update statement:
UPDATE myFirstTable
SET wage_level = (ABS(CHECKSUM(NEWID())) % 5) + 1
Check the update:
SELECT *
FROM myFirstTable
Results:
id city wage_level
1 Denver 3
2 Chicago 3
3 Seattle 2
4 Los Angeles 4
5 Boise 3
Explanation: use NEWID() to generate a guid, CHECKSUM() to get a number based on that guid, ABS() to get only positive values, % 5 to get only values between 0 and 4, and finally, + 1 to get only values between 1 and 5:

CONVERT varchar to decimal not working on SQL Server

1st of all I am working on SQL Server 2012 Express 64-bit.
I am trying to convert a varchar field to decimal and multiply to another varchar too.
This is the code I am trying to run on a SELECT query:
(CONVERT(decimal(12,2), COALESCE(Lineas.PARAM1, 0.00))) * (CONVERT(decimal(12,2), COALESCE(Lineas.PARAM2, 0.00))) AS 'MULTIPARAM'
Where PARAM1 and PARAM2 are varchar.
The numbers on this fields are 1 and 2. So simple. I want it to return 3.00 but the shown error is:
Mens. 8114, Nivel 16, Estado 5, LĂ­nea 1
Error al convertir el tipo de datos varchar a numeric.
I just want to get the result of PARAM1 * PARAM2 even if PARAM1 or PARAM2 are null (being converted to 0) or if they're decimal (separated with dot, for example: 100.5)
I don't get why it isn't working... Thanks!
FOUND THE ERROR:
COALESCE(Lineas.PARAM1, 0.00) makes it fail "converting varchar to numeric"; and COALESCE(Lineas.PARAM1, 0) makes it fail "converting varchar '100.5' to int." (which is not my intention anyway)
How could I make this "work"?
SOLUTION BY JATIN:
SELECT (COALESCE(CONVERT(decimal(12,2), NULLIF(REPLACE(Lineas.PARAM1,' ',''),'')), 0.00)) * (COALESCE(CONVERT(decimal(12,2), NULLIF(REPLACE(Lineas.PARAM2,' ',''),'')), 0.00)) AS 'MULTIPARAM'
what i did is first REPLACE all spaces and then if it is empty string then set it to NULL, rest is the same
Try this...
SELECT (COALESCE(CONVERT(decimal(12,2), Lineas.PARAM1), 0.00)) * (COALESCE(CONVERT(decimal(12,2), Lineas.PARAM2), 0.00)) AS 'MULTIPARAM'
the issue is here..
SELECT COALESCE('1', 0.00)
if still you get error "Error converting data type varchar to numeric", then your data has some characters else than 0-9.
You're going to kick yourself.
Error message in English;
Msg 8115, Level 16, State 8, Line 12
Arithmetic overflow error converting varchar to data type numeric.
Sample Data;
IF OBJECT_ID('tempdb..#TestData') IS NOT NULL DROP TABLE #TestData
GO
CREATE TABLE #TestData (ID int, Field1 varchar(10), Field2 varchar(10))
INSERT INTO #TestData
VALUES
(1,1,1)
,(2,1,2)
,(3,1,3)
,(4,1,4)
,(5,2,1)
,(6,2,2)
,(7,2,3)
Query (remove the decimal places from your coalesce);
SELECT
ID
,Field1
,Field2
,(CONVERT(decimal(12,2), COALESCE(a.Field1, 0))) * (CONVERT(decimal(12,2), COALESCE(a.Field2, 0))) AS 'MULTIPARAM'
FROM #TestData a
Result;
ID Field1 Field2 MULTIPARAM
1 1 1 1.0000
2 1 2 2.0000
3 1 3 3.0000
4 1 4 4.0000
5 2 1 2.0000
6 2 2 4.0000
7 2 3 6.0000
If you want the result to two decimals then use this;
SELECT
ID
,Field1
,Field2
,CONVERT(decimal(12,2),(CONVERT(decimal(12,2), COALESCE(a.Field1, 0))) * (CONVERT(decimal(12,2), COALESCE(a.Field2, 0)))) AS 'MULTIPARAM'
FROM #TestData a
Which gives these results;
ID Field1 Field2 MULTIPARAM
1 1 1 1.00
2 1 2 2.00
3 1 3 3.00
4 1 4 4.00
5 2 1 2.00
6 2 2 4.00
7 2 3 6.00
If you have decimals in the varchar field, then give this a go;
CONVERT(decimal(12,2),COALESCE(CAST(a.Field1 AS decimal(12,2)), 0) * COALESCE(CAST(a.Field2 AS decimal(12,0)), 0)) AS 'MULTIPARAM'

Resources