Splitting single row into multiple based on column - sql

I have certain number of rows as below:
| Material No | Quantity | Weight | Unit |
--------------------------------------------
| 111-22283/4 | 2 | 53 | kg |
| 123-ABC45/7 | 5 | 41 | g |
| 133-67879/80 | 7 | 31 | g |
| 144-54628 | 1 | 14 | kg |
Now I want to produce output like below:
| Material No | Quantity | Weight | Unit |
--------------------------------------------
| 111-22283 | 2 | 53 | kg |
| 111-22284 | 2 | 53 | kg |
| 123-ABC45 | 5 | 41 | g |
| 123-ABC46 | 5 | 41 | g |
| 123-ABC47 | 5 | 41 | g |
| 133-67879 | 7 | 31 | g |
| 133-67880 | 7 | 31 | g |
| 144-54628 | 1 | 14 | kg |
Logic: Based on the material no I have to split the rows. If '/' at end of the material no then it needs to be spilt. Then we have to find the difference b/w last number in the material no and the number /. If it is 2 then I want 2 different rows with each number as material number(Means if the last digit is 83/4 then I want material number ends with 83 and 84). The tricky part is when we are having 89/90. It will contain 2 number after / (only for 10's,20's etc.,). All other column will remain as same for each material no.
To achieve this ,currently we have a very big procedure which contains around 50-80 lines of code( find the row with '/' and remove separately then find the index of / and so on). I would like to know if this can be done with simple query or a very short procedure.

the challenging part is probably splitting the material no to get the starting and ending number.
After that, it is just a simple recursive query to increment the number and concatenate back to form the material no.
;with rcte as
(
select MaterialNo,
base, st, en, n = st,
material = convert(varchar(20), base + isnull(convert(varchar(10), st), ''))
from material m
-- get the position of the `/`
cross apply
(
select split = charindex('/', MaterialNo)
) s
-- extract the ending number and convert to integer
cross apply
(
select en = case when split > 0
then convert(int, right(MaterialNo, len(MaterialNo) - split))
end
) en
-- extract the starting number and convert to integer
cross apply
(
select st = case when split > 0
then convert(int, substring(MaterialNo, split - len(en), len(en)))
end
) st
-- extract the base material no for concatenate
cross apply
(
select base = case when split > 0
then left(MaterialNo, split - len(en) - 1)
else MaterialNo
end
) b
union all
select MaterialNo, base, st, en,
n = n + 1,
material = convert(varchar(20), base + convert(varchar(10), n + 1))
from rcte
where n < en
)
select *
from rcte
order by MaterialNo, material
all this is based on the assumption that the ending of material no is purely numeric.
Note : if you have a tally table, you can use that to replace the recursive cte

UPDATE
When a value from MATERIALNO can represent a series of values (as the updated question) :
DDL
CREATE FUNCTION UFN_STRTOSERIES (#materialNo VARCHAR(80), #qte INT, #weight INT, #unit VARCHAR(50))
RETURNS #result TABLE (
MATERIALNO VARCHAR(80),
QTE INT,
Weight INT,
UNIT VARCHAR(50)
)
AS
BEGIN
DECLARE #base VARCHAR(50) = LEFT(#materialNo,CHARINDEX('/',#materialNo)-1-LEN(RIGHT(#materialNo,LEN(#materialNo)-CHARINDEX('/',#materialNo))));
DECLARE #start INT = CONVERT( INT , RIGHT( LEFT(#materialNo,CHARINDEX('/',#materialNo)-1) , LEN(RIGHT(#materialNo,LEN(#materialNo)-CHARINDEX('/',#materialNo))) ) );
DECLARE #end INT = CONVERT( INT , RIGHT(#materialNo,LEN(#materialNo)-CHARINDEX('/',#materialNo)) );
DECLARE #i INT = #start;
WHILE #i <= #end
BEGIN
INSERT #result
SELECT CONCAT(#base,#i) AS MATERIALNO, #qte, #weight, #unit;
SET #i = #i + 1;
END;
RETURN
END;
CREATE TABLE MATERIALS
(
MATERIALNO VARCHAR(80),
QTE INT,
Weight INT,
UNIT VARCHAR(50)
)
INSERT INTO MATERIALS VALUES
('111-22283/4',2,53,'kg'),
('123-33345/7',5,41,'g' ),
('123-ABC45/7',5,41,'g'),
('133-67879/80',7,31,'g'),
('144-54628',1,14,'kg')
DML
SELECT MATERIALNO,QTE,Weight,UNIT
FROM (
SELECT MATERIALNO,QTE,Weight,UNIT
FROM MATERIALS
WHERE CHARINDEX('/',MATERIALNO) < 1
UNION
SELECT series.MATERIALNO,series.QTE,series.Weight,series.UNIT
FROM MATERIALS m
CROSS APPLY UFN_STRTOSERIES(MATERIALNO,QTE,Weight,UNIT) series
WHERE CHARINDEX('/',m.MATERIALNO) > 1
) base
ORDER BY base.MATERIALNO
OLD ANSWER
When a value from MATERIALNO represents only two values :
UNION is the easiest answer (then you have to check performances) :
DDL
CREATE TABLE MATERIALS
(
MATERIALNO VARCHAR(80),
QTE INT,
Weight INT,
UNIT VARCHAR(50)
)
INSERT INTO MATERIALS VALUES
('111-22283/4',2,53,'kg'),
('123-33345/7',5,41,'g' ),
('133-67879/80',7,31,'g'),
('144-54628',1,14,'kg' )
Query
SELECT MATERIALNO,QTE,Weight,UNIT
FROM (
SELECT MATERIALNO,QTE,Weight,UNIT
FROM MATERIALS
WHERE CHARINDEX('/',MATERIALNO) < 1
UNION
SELECT LEFT(MATERIALNO,CHARINDEX('/',MATERIALNO)-1),QTE,Weight,UNIT
FROM MATERIALS
WHERE CHARINDEX('/',MATERIALNO) > 1
UNION
SELECT
CONCAT(LEFT(MATERIALNO,CHARINDEX('/',MATERIALNO)-1-LEN(RIGHT(MATERIALNO,LEN(MATERIALNO)-CHARINDEX('/',MATERIALNO)))),RIGHT(MATERIALNO,LEN(MATERIALNO)-CHARINDEX('/',MATERIALNO))) AS MATERIALNO
,QTE,Weight,UNIT
FROM MATERIALS
WHERE CHARINDEX('/',MATERIALNO) > 1
) BASE
ORDER BY BASE.MATERIALNO
Fiddle : http://sqlfiddle.com/#!18/8e768/2

Related

Generate random numbers in a specific range without duplicate values

SELECT CEILING (RAND(CAST(NEWID() AS varbinary)) *275) AS RandomNumber
This creates random numbers. However, it spits out duplicates
Generate a numbers table with the range of your desire. In my case, I do it via recursive cte. Then order the numbers table using the newid function.
with numbers as (
select 0 as val union all
select val + 1 from numbers where val < 275
)
select ord = row_number() over(order by ap.nid),
val
into #rands
from numbers n
cross apply (select nid = newid()) ap
order by ord
option (maxrecursion 1000);
One run of the code above results in a table of 276 values that begins and ends as follows:
| ord | val |
+-----+-----+
| 1 | 102 |
| 2 | 4 |
| 3 | 127 |
| ... | ... |
| 276 | 194 |
Non duplicating ordering of random numbers.
You can select from it a variety of ways, but one way could be:
-- initiate these to begin with
declare #ord int = 1;
declare #val int;
declare #rand int;
-- do this on every incremental need for a random number
select #val = val,
#ord = #ord + 1
from #rands
where ord = #ord;
print #val;
In the comments to my other answer, you write:
The table I'm working with has an ID , Name , and I want to generate a 3rd column that assigns a unique random number between 1-275 (as there are 275 rows) with no duplicates.
In the future, please include details like this in your original question. With this information, we can help you out better.
First, let's make a sample of your problem. We'll simplify it to just 5 rows, not 275:
create table #data (
id int,
name varchar(10)
);
insert #data values
(101, 'Amanda'),
(102, 'Beatrice'),
(103, 'Courtney'),
(104, 'Denise'),
(105, 'Elvia');
Let's now add the third column you want:
alter table #data add rando int;
Finally, let's update the table by creating a subquery that orders the rows randomly using row_number(), and applying the output the the column we just created:
update reordered
set rando = rowNum
from (
select *,
rowNum = row_number() over(order by newid())
from #data
) reordered;
Here's the result I get, but of course it will be different every time it is run:
select *
from #data
| id | name | rando |
+-----+----------+-------+
| 101 | Amanda | 3 |
| 102 | Beatrice | 1 |
| 103 | Courtney | 4 |
| 104 | Denise | 5 |
| 105 | Elvia | 2 |

How to generate hours and populate in sql table for a period

How to create a table with columns having date,hour as a separate fields for a period of time say 10 years from 1st Jan 2010 to 1st Jan 2020.
I have created calendar table referring to url :
click here
It generates only dates,day,week,month but not time.
I want to generate new column with the value as 0,...23 hours for every day.
You can achieve it an another easy way as follows
;WITH CTE AS (
SELECT NUMBER FROM master.dbo.spt_values where TYPE='P' AND NUMBER BETWEEN 1 AND 2047
)
, YEARS AS(
SELECT NUMBER+2009 AS Y FROM CTE WHERE NUMBER BETWEEN 1 AND 11
)
,MONTHS AS (
SELECT NUMBER AS M FROM CTE WHERE NUMBER BETWEEN 1 AND 12
)
,DAYSS AS(
SELECT NUMBER AS D FROM CTE WHERE NUMBER BETWEEN 1 AND 31
)
,HRS AS (
SELECT NUMBER AS H FROM CTE WHERE NUMBER BETWEEN 1 AND 24
)
SELECT * FROM YEARS Y
CROSS APPLY MONTHS M
CROSS APPLY (SELECT D FROM DAYSS
WHERE D<= DATEDIFF(DD,DATEFROMPARTS(Y.Y, M.M,01),DATEADD(MM,1, DATEFROMPARTS(Y.Y, M.M,01))))D
CROSS APPLY HRS H
ORDER BY Y.Y, M.M, D.D, H.H
Sample Result:
+------+---+----+----+
| Y | M | D | H |
+------+---+----+----+
| -- | - | -- | -- |
| 2020 | 2 | 29 | 21 |
| 2020 | 2 | 29 | 22 |
| 2020 | 2 | 29 | 23 |
| 2020 | 2 | 29 | 24 |
| 2020 | 3 | 1 | 1 |
| 2020 | 3 | 1 | 2 |
| 2020 | 3 | 1 | 3 |
| -- | --| -- | -- |
+------+---+----+----+
Note: DATEFROMPARTS function will work only with SQL Server 2012 and
above. You can replace it by concatinating them and converting to date
field for lower versions.
Either create another table that holds 24 rows for each hour of the day or use a derived table like so
SELECT
H.[Hour]
,D.[Date]
FROM
dbo.DateDimension D
CROSS APPLY
( --Derived table with 24 rows
SELECT
[Hour] = V.number
FROM
master..spt_values V
WHERE
V.type = 'P'
AND
V.number >= 1 AND V.number <= 24
) H
WHERE
[D].[Date] = '2000-01-01'
Output
Create a time table as well as your Calendar table. How specific you get with your times is up to you (for example, mine has a value for every second in a day). Then, simple JOIN to too:
SELECT * --Yes, * is bad, but I don't know what the OP's Columns are.
--The OP will need to replace the * with the appropriate columns
FROM CalendarTable CT
JOIN TimeTable TT ON TT.[Minutes] = 0 AND TT.[Seconds] = 0
WHERE CT.DateValue >= '20100101'
AND CT.DateValue < '20200102';
The filter means that only the Hour values would be returned.
Also, it's worth noting there is no hour 24 in a day. The hours are 0 - 23, not 1-24.
I would do it like this:
--set start variables
declare #period_length_days as int = 21
declare #start_dt as datetime = getdate()
declare #end_dt as datetime = dateadd(day,#period_length_days,getdate())
--declare table
declare #my_calender table (recid int identity(1,1),
my_datetime datetime,
my_year int,
my_month int,
my_day int,
my_hour int)
--of course you can add multiple more: iso_wk, minute, second etc etc
--fill table
declare #loop_dt as datetime = #start_dt
--loop from start_dt to end_dt, by adding 1 hour each cycle, until loop_dt = end_dt
while #loop_dt <> #end_dt
begin
insert into #my_calender
select
#loop_dt
, datepart(year,#loop_dt)
, datepart(month,#loop_dt)
, datepart(day,#loop_dt)
, datepart(hour,#loop_dt)
set #loop_dt = dateadd(hour,1,#loop_dt)
end
select * from #my_calender

SQL SELECT: concatenated column with line breaks and heading per group

I have the following SQL result from a SELECT query:
ID | category| value | desc
1 | A | 10 | text1
2 | A | 11 | text11
3 | B | 20 | text20
4 | B | 21 | text21
5 | C | 30 | text30
This result is stored in a temporary table named #temptab. This temporary table is then used in another SELECT to build up a new colum via string concatenation (don't ask me about the detailed rationale behind this. This is code I took from a colleague). Via FOR XML PATH() the output of this column is a list of the results and is then used to send mails to customers.
The second SELECT looks as follows:
SELECT t1.column,
t2.column,
(SELECT t.category + ' | ' + t.value + ' | ' + t.desc + CHAR(9) + CHAR(13) + CHAR(10)
FROM #temptab t
WHERE t.ID = ttab.ID
FOR XML PATH(''),TYPE).value('.','NVARCHAR(MAX)') AS colname
FROM table1 t1
...
INNER JOIN #temptab ttab on ttab.ID = someOtherTable.ID
...
Without wanting to go into too much detail, the column colname becomes populated with several entries (due to multiple matches) and hence, a longer string is stored in this column (CHAR(9) + CHAR(13) + CHAR(10) is essentially a line break). The result/content of colname looks like this (it is used to send mails to customers):
A | 10 | text1
A | 11 | text11
B | 20 | text20
B | 21 | text21
C | 30 | text30
Now I would like to know, if there is a way to more nicely format this output string. The best case would be to group the same categories together and add a heading and empty line between different categories:
*A*
A | 10 | text1
A | 11 | text11
*B*
B | 20 | text20
B | 21 | text21
*C*
C | 30 | text30
My question is: How do I have to modify the above query (especially the string-concatenation-part) to achieve above formatting? I was thinking about using a GROUP BY statement, but this obviously does not yield the desired result.
Edit: I use Microsoft SQL Server 2008 R2 (SP2) - 10.50.4270.0 (X64)
Declare #YourTable table (ID int,category varchar(50),value int, [desc] varchar(50))
Insert Into #YourTable values
(1,'A',10,'text1'),
(2,'A',11,'text11'),
(3,'B',20,'text20'),
(4,'B',21,'text21'),
(5,'C',30,'text30')
Declare #String varchar(max) = ''
Select #String = #String + Case when RowNr=1 Then Replicate(char(13)+char(10),2) +'*'+Category+'*' Else '' end
+ char(13)+char(10) + category + ' | ' + cast(value as varchar(25)) + ' | ' + [desc]
From (
Select *
,RowNr=Row_Number() over (Partition By Category Order By Value)
From #YourTable
) A Order By Category, Value
Select Substring(#String,5,Len(#String))
Returns
*A*
A | 10 | text1
A | 11 | text11
*B*
B | 20 | text20
B | 21 | text21
*C*
C | 30 | text30
This should return what you want
Declare #YourTable table (ID int,category varchar(50),value int, [desc] varchar(50))
Insert Into #YourTable values
(1,'A',10,'text1'),
(2,'A',11,'text11'),
(3,'B',20,'text20'),
(4,'B',21,'text21'),
(5,'C',30,'text30');
WITH Categories AS
(
SELECT category
,'**' + category + '**' AS CatCaption
,ROW_NUMBER() OVER(ORDER BY category) AS CatRank
FROM #YourTable
GROUP BY category
)
,Grouped AS
(
SELECT c.CatRank
,0 AS ValRank
,c.CatCaption AS category
,-1 AS ID
,'' AS Value
,'' AS [desc]
FROM Categories AS c
UNION ALL
SELECT c.CatRank
,ROW_NUMBER() OVER(PARTITION BY t.category ORDER BY t.Value)
,t.category
,t.ID
,CAST(t.value AS VARCHAR(100))
,t.[desc]
FROM #YourTable AS t
INNER JOIN Categories AS c ON t.category=c.category
)
SELECT category,Value,[desc]
FROM Grouped
ORDER BY CatRank,ValRank
The result
category Value desc
**A**
A 10 text1
A 11 text11
**B**
B 20 text20
B 21 text21
**C**
C 30 text30

Make single record to multiple records in sql server

I have a records look like below
From two rows, I want to split ShiftPattern values and create multiple records and StartWeek will be created sequentially.
Final Query:
Split ShiftPattern Column and Create multiple records
Increase StartWeek like as 20, 21 to rotation.
Output result
This is what you need. Tested in fiddle.
SQLFiddle Demo
select q.locationid,q.employeeid,
case
when (lag(employeeid,1,null) over (partition by employeeid order by weekshiftpatternid)) is null
then startweek
else startweek + 1
end as rotation ,
q.weekshiftpatternid,
q.shiftyear
from
(
select locationid,employeeid, left(d, charindex(',', d + ',')-1) as weekshiftpatternid ,
startweek,shiftyear
from (
select *, substring(shiftpattern, number, 200) as d from MyTable locationid left join
(select distinct number from master.dbo.spt_values where number between 1 and 200) col2
on substring(',' + shiftpattern, number, 1) = ','
) t
) q
Output
+------------+------------+----------+--------------------+-----------+
| locationid | employeeid | rotation | weekshiftpatternid | shiftyear |
+------------+------------+----------+--------------------+-----------+
| 1 | 10000064 | 20 | 1006 | 2016 |
| 1 | 10000064 | 21 | 1008 | 2016 |
| 1 | 10000065 | 20 | 1006 | 2016 |
| 1 | 10000065 | 21 | 1008 | 2016 |
+------------+------------+----------+--------------------+-----------+
Similar:
In my test table my ID is your EmployeeID or however you want to work it.
SELECT
*,
LEFT(shiftBits, CHARINDEX(',', shiftBits + ',')-1) newShiftPattern,
StartWeek+ROW_NUMBER() OVER(PARTITION BY ID ORDER BY shiftBits ) as newStartWeek
FROM
(
SELECT
SUBSTRING(shiftPattern, number, LEN(shiftPattern)) AS shiftBits,
test2.*
FROM
test2,master.dbo.spt_values
WHERE
TYPE='P' AND number<LEN(shiftPattern)
AND SUBSTRING(',' + shiftPattern, number, 1) = ','
) AS x

SQL Server - Insert lines with null values when month doesn't exist

I have a table like this one:
Yr | Mnth | W_ID | X_ID | Y_ID | Z_ID | Purchases | Sales | Returns |
2015 | 10 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2015 | 12 | 1 | 5210 | 1402 | 2 | 12000.00 | etc | etc |
2016 | 1 | 1 | 5210 | 1402 | 2 | 1000.00 | etc | etc |
2016 | 3 | 1 | 5210 | 1402 | 2 | etc | etc | etc |
2014 | 3 | 9 | 880 | 2 | 7 | etc | etc | etc |
2014 | 12 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 5 | 9 | 880 | 2 | 7 | etc | etc | etc |
2015 | 7 | 9 | 880 | 2 | 7 | etc | etc | etc |
For each combination of (W, X, Y, Z) I would like to insert the months that don't appear in the table and are between the first and last month.
In this example, for combination (W=1, X=5210, Y=1402, Z=2), I would like to have additional rows for 2015/11 and 2016/02, where Purchases, Sales and Returns are NULL. For combination (W=9, X=880, Y=2, Z=7) I would like to have additional rows for months between 2014/4 and 2014/11, 2015/01 and 2015/04, 2016/06.
I hope I have explained myself correctly.
Thank you in advance for any help you can provide.
The process is rather cumbersome in this case, but quite possible. One method uses a recursive CTE. Another uses a numbers table. I'm going to use the latter.
The idea is:
Find the minimum and maximum values for the year/month combination for each set of ids. For this, the values will be turned into months since time 0 using the formula year*12 + month.
Generate a bunch of numbers.
Generate all rows between the two values for each combination of ids.
For each generated row, use arithmetic to re-extract the year and month.
Use left join to bring in the original data.
The query looks like:
with n as (
select row_number() over (order by (select null)) - 1 as n -- start at 0
from master.spt_values
),
minmax as (
select w_id, x_id, y_id, z_id, min(yr*12 + mnth) as minyyyymm,
max(yr*12 + mnth) as maxyyyymm
from t
group by w_id, x_id, y_id, z_id
),
wxyz as (
select minmax.*, minmax.minyyyymm + n.n,
(minmax.minyyyymm + n.n) / 12 as yyyy,
((minmax.minyyyymm + n.n) % 12) + 1 as mm
from minmax join
n
on minmax.minyyyymm + n.n <= minmax.maxyyyymm
)
select wxyz.yyyy, wxyz.mm, wxyz.w_id, wxyz.x_id, wxyz.y_id, wxyz.z_id,
<columns from t here>
from wxyz left join
t
on wxyz.w_id = t.w_id and wxyz.x_id = t.x_id and wxyz.y_id = t.y_id and
wxyz.z_id = t.z_id and wxyz.yyyy = t.yr and wxyz.mm = t.mnth;
Thank you for your help.
Your solution works, but I noticed it is not very good in terms of performance, but meanwhile I have managed to get a solution for my problem.
DECLARE #start_date DATE, #end_date DATE;
SET #start_date = (SELECT MIN(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
SET #end_date = (SELECT MAX(EOMONTH(DATEFROMPARTS(Yr , Mnth, 1))) FROM Table_Input);
DECLARE #tdates TABLE (Period DATE, Yr INT, Mnth INT);
WHILE #start_date <= #end_date
BEGIN
INSERT INTO #tdates(PEriod, Yr, Mnth) VALUES(#start_date, YEAR(#start_date), MONTH(#start_date));
SET #start_date = EOMONTH(DATEADD(mm,1,DATEFROMPARTS(YEAR(#start_date), MONTH(#start_date), 1)));
END
DECLARE #pks TABLE (W_ID NVARCHAR(50), X_ID NVARCHAR(50)
, Y_ID NVARCHAR(50), Z_ID NVARCHAR(50)
, PerMin DATE, PerMax DATE);
INSERT INTO #pks (W_ID, X_ID, Y_ID, Z_ID, PerMin, PerMax)
SELECT W_ID, X_ID, Y_ID, Z_ID
, MIN(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMin
, MAX(EOMONTH(DATEFROMPARTS(Ano, Mes, 1))) AS PerMax
FROM Table1
GROUP BY W_ID, X_ID, Y_ID, Z_ID;
INSERT INTO Table_Output(W_ID, X_ID, Y_ID, Z_ID
, ComprasLiquidas, RTV, DevManuais, ComprasBrutas, Vendas, Stock, ReceitasComerciais)
SELECT TP.DB, TP.Ano, TP.Mes, TP.Supplier_Code, TP.Depart_Code, TP.BizUnit_Code
, TA.ComprasLiquidas, TA.RTV, TA.DevManuais, TA.ComprasBrutas, TA.Vendas, TA.Stock, TA.ReceitasComerciais
FROM
(
SELECT W_ID, X_ID, Y_ID, Z_ID
FROM #tdatas CROSS JOIN #pks
WHERE Period BETWEEN PerMin And PerMax
) AS TP
LEFT JOIN Table_Input AS TA
ON TP.W_ID = TA.W_ID AND TP.X_ID = TA.X_ID AND TP.Y_ID = TA.Y_ID
AND TP.Z_ID = TA.Z_ID
AND TP.Yr = TA.Yr
AND TP.Mnth = TA.Mnth
ORDER BY TP.W_ID, TP.X_ID, TP.Y_ID, TP.Z_ID, TP.Yr, TP.Mnth;
I do the following:
Get the Min and Max date of the entire table - #start_date and #end_date variables;
Create an auxiliary table with all dates between Min and Max - #tdates table;
Get all the combinations of (W_ID, X_ID, Y_ID, Z_ID) along with the min and max dates of that combination - #pks table;
Create the cartesian product between #tdates and #pks, and in the WHERE clause I filter the results between the Min and Max of the combination;
Compute a LEFT JOIN of the cartesian product table with the input data table.