Related
I'd like to split comma-delimited strings in SQL Server 2012. I'm interested in an XML solution, not a function or while loop (performance and permissions reasons). I read this post: STRING_SPLIT in SQL Server 2012 which was helpful, however, my context is not splitting a variable but rather a column in a table. Below is an example of the kind of dataset I'm working with:
CREATE TABLE #EXAMPLE
(
ID INT,
LIST VARCHAR(1000)
)
INSERT INTO #EXAMPLE
VALUES (1, '12345,54321'), (2, '48965'), (3, '98765,45678,15935'), (4, '75315')
SELECT * FROM #EXAMPLE
DROP TABLE #EXAMPLE
Given that dataset, how could I go about splitting the LIST field on the comma so that I get this data set?
CREATE TABLE #EXAMPLE
(
ID INT,
LIST VARCHAR(1000)
)
INSERT INTO #EXAMPLE
VALUES (1, '12345'), (1, '54321'), (2, '48965'), (3, '98765'), (3, '45678'), (3, '15935'), (4, '75315')
SELECT * FROM #EXAMPLE
DROP TABLE #EXAMPLE
I feel like I'm blanking on implementing this with a table column as opposed to a variable, but I'm sure it's pretty similar. I'd be greatly appreciative of any input. Thanks!
If you want an XML solution the following should hopefully suffice.
Note - this is easily wrapped in a reusable table-valued function however you state you don't want a function so just using in-line.
select e.id, s.List
from #example e
cross apply (
select List = y.i.value('(./text())[1]', 'varchar(max)')
from (
select x = convert(xml, '<i>' + replace(e.list, ',', '</i><i>') + '</i>').query('.')
) as a cross apply x.nodes('i') as y(i)
)s
See working Fiddle
Taking into account your link, this can be done by slightly changing the query by adding Cross Apply.
Select e.ID, t.a
From #Example As e Cross Apply (
SELECT Split.a.value('.', 'NVARCHAR(MAX)') DATA
FROM
(
SELECT CAST('<X>'+REPLACE(e.List, ',', '</X><X>')+'</X>' AS XML) AS String
) AS A
CROSS APPLY String.nodes('/X') AS Split(a)) As t(a)
As #Charlieface already mentioned there is a risk to bump into XML entities: ampersand and the like.
That's why I always use a CDATA section for safety.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT, LIST VARCHAR(1000));
INSERT INTO #tbl VALUES
(1, '12345,<54321'),
(2, '48965'),
(3, '98765,45678,15935'),
(4, '75315');
-- DDL and sample data population, end
SELECT e.id, s.List
FROM #tbl e
CROSS APPLY (
SELECT List = y.i.value('(./text())[1]', 'VARCHAR(MAX)')
FROM (
SELECT x = TRY_CAST('<i><![CDATA[' + REPLACE(e.list, ',', ']]></i><i><![CDATA[') + ']]></i>' AS XML)
) AS a CROSS APPLY x.nodes('i') as y(i)
) AS s;
Output
+----+--------+
| id | List |
+----+--------+
| 1 | 12345 |
| 1 | <54321 |
| 2 | 48965 |
| 3 | 98765 |
| 3 | 45678 |
| 3 | 15935 |
| 4 | 75315 |
+----+--------+
I have to process data from xml by date of operation, simulating day by day, during 4th months process in the data base with a XML file that looks like this:
<Operaciones_por_Dia>
<OperacionDia fecha="2020-01-30">
<PagoRecibo TipoRecibo="5" NumFinca="9782331"/>
<PagoRecibo TipoRecibo="5" NumFinca="6696849"/>
<TransConsumo id="1" LecturaM3="325" descripcion="Cobro Mensual" NumFinca="3336538"/>
<TransConsumo id="3" LecturaM3="40" descripcion="Lectura errĂ³nea" NumFinca="2425954"/>
</OperacionDia>
<OperacionDia fecha="2020-04-08">
<PagoRecibo TipoRecibo="7" NumFinca="1423800"/>
<PagoRecibo TipoRecibo="7" NumFinca="1393022"/>
<TransConsumo id="2" LecturaM3="22" descripcion="Reclamo de cliente" NumFinca="2101885"/>
</OperacionDia>
</Operaciones_por_Dia>
When I prepare the data, I store all the dates from XML into a variable table and then I iterate from the min date throught the max date (#FechaOperacion has stored that date), then I have to process the node data where the date match with the date from xml node .
I try it doing this, but #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE')
don't match well, the correct data that I have to compare is Fecha with #FechaOperacion, but I don't know how can I get that value with Xquery.
INSERT #PagosHoy (NumFinca, TipoRecibo, Fecha)
select ph.value('#NumFinca', 'INT')
, ph.value('#TipoRecibo', 'INT')
, ph.value('../#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') AS t(ph)
where #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE') = #FechaOperacion
With the following code it work well, but I want to know how to do it like the other way.
INSERT INTO #PagosHoy(NumFinca,TipoRecibo,Fecha)
SELECT [NumFinca],[TipoRecibo],[fechaDeIngreso]
FROM OPENXML (#hdoc, 'Operaciones_por_Dia/OperacionDia/PagoRecibo',1)
WITH ( [NumFinca] VARCHAR(30) '#NumFinca',
[TipoRecibo] INT '#TipoRecibo',
[fechaDeIngreso] VARCHAR(100) '../#fecha')
WHERE [fechaDeIngreso] = #FechaOperacion
EXEC spProcesaPagos #PagosHoy
Microsoft proprietary OPENXML and its companions sp_xml_preparedocument and sp_xml_removedocument are kept just for backward compatibility with the obsolete SQL Server 2000. It is strongly recommended to re-write your SQL and switch it to XQuery.
Here is another method similar to what is proposed by #AlwaysLearning, but more simple.
It is using XPath predicate instead of WHERE clause.
SQL
-- DDL and sample data population, start
DECLARE #PagosHoy TABLE (ID INT IDENTITY PRIMARY KEY, NumFinca INT, TipoRecibo INT, Fecha DATE);
DECLARE #xml XML =
N'<Operaciones_por_Dia>
<OperacionDia fecha="2020-01-30">
<PagoRecibo TipoRecibo="5" NumFinca="9782331"/>
<PagoRecibo TipoRecibo="5" NumFinca="6696849"/>
<TransConsumo id="1" LecturaM3="325" descripcion="Cobro Mensual"
NumFinca="3336538"/>
<TransConsumo id="3" LecturaM3="40" descripcion="Lectura errĂ³nea"
NumFinca="2425954"/>
</OperacionDia>
<OperacionDia fecha="2020-04-08">
<PagoRecibo TipoRecibo="7" NumFinca="1423800"/>
<PagoRecibo TipoRecibo="7" NumFinca="1393022"/>
<TransConsumo id="2" LecturaM3="22" descripcion="Reclamo de cliente"
NumFinca="2101885"/>
</OperacionDia>
</Operaciones_por_Dia>';
-- DDL and sample data population, end
DECLARE #FechaOperacion DATE = '2020-01-30';
INSERT #PagosHoy (NumFinca, TipoRecibo, Fecha)
SELECT c.value('#NumFinca', 'INT')
, c.value('#TipoRecibo', 'INT')
, #FechaOperacion AS FechaOperacion
FROM #xml.nodes('/Operaciones_por_Dia/OperacionDia[#fecha eq sql:variable("#FechaOperacion")]/PagoRecibo') AS t(c)
-- test
SELECT * FROM #PagosHoy;
Output
+----+----------+------------+------------+
| ID | NumFinca | TipoRecibo | Fecha |
+----+----------+------------+------------+
| 1 | 9782331 | 5 | 2020-01-30 |
| 2 | 6696849 | 5 | 2020-01-30 |
+----+----------+------------+------------+
The where clause in your first example queries /Operaciones_por_Dia/OperacionDia/#fecha[1] independently of the nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') query so will always be comparing #FechaOperacion against the first date attribute - which is 2020-01-30 in this document.
declare #FechaOperacion date = convert(date, '2020-01-30', 120);
select
ph.value('#NumFinca', 'INT'),
ph.value('#TipoRecibo', 'INT'),
ph.value('../#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia/PagoRecibo') AS t(ph)
where #DocumentoXML.value('(/Operaciones_por_Dia/OperacionDia/#fecha)[1]', 'DATE') = #FechaOperacion
Depending on the value of #FechaOperacion it will either return all rows in the document (when it matches) or now rows at all (when it doesn't)...
(No column name) (No column name) (No column name)
---------------- ---------------- ----------------
9782331 5 2020-01-30
6696849 5 2020-01-30
1423800 7 2020-04-08
1393022 7 2020-04-08
The solution is to query for nodes('/Operaciones_por_Dia/OperacionDia') and then use a cross apply to query the PagoRecibo child nodes.
declare #FechaOperacion date = convert(date, '2020-01-30', 120);
select
ph.value('#NumFinca', 'INT'),
ph.value('#TipoRecibo', 'INT'),
od.value('#fecha', 'DATE')
from #DocumentoXML.nodes('/Operaciones_por_Dia/OperacionDia') AS s(od)
cross apply s.od.nodes('PagoRecibo') AS t(ph)
where s.od.value('#fecha', 'DATE') = #FechaOperacion
Which returns...
(No column name) (No column name) (No column name)
---------------- ---------------- ----------------
9782331 5 2020-01-30
6696849 5 2020-01-30
I have two tables and the values like this
`create table InputLocationTable(SKUID int,InputLocations varchar(100),Flag varchar(100))
create table Location(SKUID int,Locations varchar(100))
insert into InputLocationTable(SKUID,InputLocations) values(11,'Loc1, Loc2, Loc3, Loc4, Loc5, Loc6')
insert into InputLocationTable(SKUID,InputLocations) values(12,'Loc1, Loc2')
insert into InputLocationTable(SKUID,InputLocations) values(13,'Loc4,Loc5')
insert into Location(SKUID,Locations) values(11,'Loc3')
insert into Location(SKUID,Locations) values(11,'Loc4')
insert into Location(SKUID,Locations) values(11,'Loc5')
insert into Location(SKUID,Locations) values(11,'Loc7')
insert into Location(SKUID,Locations) values(12,'Loc10')
insert into Location(SKUID,Locations) values(12,'Loc1')
insert into Location(SKUID,Locations) values(12,'Loc5')
insert into Location(SKUID,Locations) values(13,'Loc4')
insert into Location(SKUID,Locations) values(13,'Loc2')
insert into Location(SKUID,Locations) values(13,'Loc2')`
I need to get the output by matching SKUID's from Each tables and Update the value in Flag column as shown in the screenshot, I have tried something like this code
`SELECT STUFF((select ','+ Data.C1
FROM
(select
n.r.value('.', 'varchar(50)') AS C1
from InputLocation as T
cross apply (select cast('<r>'+replace(replace(Location,'&','&'), ',', '</r><r>')+'</r>' as xml)) as S(XMLCol)
cross apply S.XMLCol.nodes('r') as n(r)) DATA
WHERE data.C1 NOT IN (SELECT Location
FROM Location) for xml path('')),1,1,'') As Output`
But not convinced with output and also i am trying to avoid xml path code, because performance is not first place for this code, I need the output like the below screenshot. Any help would be greatly appreciated.
I think you need to first look at why you think the XML approach is not performing well enough for your needs, as it has actually been shown to perform very well for larger input strings.
If you only need to handle input strings of up to either 4000 or 8000 characters (non max nvarchar and varchar types respectively), you can utilise a tally table contained within an inline table valued function which will also perform very well. The version I use can be found at the end of this post.
Utilising this function we can split out the values in your InputLocations column, though we still need to use for xml to concatenate them back together for your desired format:
-- Define data
declare #InputLocationTable table (SKUID int,InputLocations varchar(100),Flag varchar(100));
declare #Location table (SKUID int,Locations varchar(100));
insert into #InputLocationTable(SKUID,InputLocations) values (11,'Loc1, Loc2, Loc3, Loc4, Loc5, Loc6'),(12,'Loc1, Loc2'),(13,'Loc4,Loc5'),(14,'Loc1');
insert into #Location(SKUID,Locations) values (11,'Loc3'),(11,'Loc4'),(11,'Loc5'),(11,'Loc7'),(12,'Loc10'),(12,'Loc1'),(12,'Loc5'),(13,'Loc4'),(13,'Loc2'),(13,'Loc2'),(14,'Loc1');
--Query
-- Derived table splits out the values held within the InputLocations column
with i as
(
select i.SKUID
,i.InputLocations
,s.item as Loc
from #InputLocationTable as i
cross apply dbo.fn_StringSplit4k(replace(i.InputLocations,' ',''),',',null) as s
)
select il.SKUID
,il.InputLocations
,isnull('Add ' -- The split Locations are then matched to those already in #Location and those not present are concatenated together.
+ stuff((select ', ' + i.Loc
from i
left join #Location as l
on i.SKUID = l.SKUID
and i.Loc = l.Locations
where il.SKUID = i.SKUID
and l.SKUID is null
for xml path('')
)
,1,2,''
)
,'No Flag') as Flag
from #InputLocationTable as il
order by il.SKUID;
Output:
+-------+------------------------------------+----------------------+
| SKUID | InputLocations | Flag |
+-------+------------------------------------+----------------------+
| 11 | Loc1, Loc2, Loc3, Loc4, Loc5, Loc6 | Add Loc1, Loc2, Loc6 |
| 12 | Loc1, Loc2 | Add Loc2 |
| 13 | Loc4,Loc5 | Add Loc5 |
| 14 | Loc1 | No Flag |
+-------+------------------------------------+----------------------+
For nvarchar input (I have different functions for varchar and max type input) this is my version of the string splitting function linked above:
create function [dbo].[fn_StringSplit4k]
(
#str nvarchar(4000) = ' ' -- String to split.
,#delimiter as nvarchar(1) = ',' -- Delimiting value to split on.
,#num as int = null -- Which value in the list to return. NULL returns all.
)
returns table
as
return
-- Start tally table with 10 rows.
with n(n) as (select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1)
-- Select the same number of rows as characters in #str as incremental row numbers.
-- Cross joins increase exponentially to a max possible 10,000 rows to cover largest #str length.
,t(t) as (select top (select len(isnull(#str,'')) a) row_number() over (order by (select null)) from n n1,n n2,n n3,n n4)
-- Return the position of every value that follows the specified delimiter.
,s(s) as (select 1 union all select t+1 from t where substring(isnull(#str,''),t,1) = #delimiter)
-- Return the start and length of every value, to use in the SUBSTRING function.
-- ISNULL/NULLIF combo handles the last value where there is no delimiter at the end of the string.
,l(s,l) as (select s,isnull(nullif(charindex(#delimiter,isnull(#str,''),s),0)-s,4000) from s)
select rn
,item
from(select row_number() over(order by s) as rn
,substring(#str,s,l) as item
from l
) a
where rn = #num
or #num is null;
go
I have tables as follows:
create table rrr_br(br_id nvarchar(10), location nvarchar(10))
create table rrr_profile(profileid nvarchar(10), br_id nvarchar(10))
insert into rrr_br values('AAA', 'CHN')
insert into rrr_br values('BBB', 'CHN')
insert into rrr_br values('CCC', 'CHN')
insert into rrr_br values('DDD', 'MDR')
insert into rrr_br values('EEE', 'MDR')
insert into rrr_br values('FFF', 'MDR')
insert into rrr_profile values('111', 'AAA')
insert into rrr_profile values('222', 'BBB')
insert into rrr_profile values('222', 'FFF')
Now i need to fetch from 'rrr_br' table..."Location" Wise Total Number of br_id and Number of br_ids not present in 'rrr_profile' table
Select location 'LOCATION',
cnt 'Total Vehicles',
cnt 'Vehicles Not Processed'
from (select location,Count(*) cnt
from rrr_br vh(nolock)
group by location ) B
Thanks.
The expected result is as follows:
Location| Total Vehicles| Vehicles Not Processed|
-------------------------------------------------
CHN | 3 | 1 |
MDR | 3 | 2 |
Use LEFT JOIN to determine if vehicle processed, COUNT(*) to include all rows and COUNT(col) to exclude null value.
SELECT rrr_br.location,
COUNT(*) AS Total,
COUNT(*) - COUNT(rrr_profile.profileid) AS NotProcessed
FROM rrr_br
LEFT JOIN rrr_profile ON rrr_br.br_id = rrr_profile.br_id
GROUP BY rrr_br.location
I am using Visaul Studio 2010 to build a Windows Forms application to maintain a table in an SQL Server 2008 database. The table is named CASHBOOK and here are the further details:
DATE | DESCRIPTION | DEBIT | CREDIT | BALANCE
--------|----------------|---------|-----------|---------
1/1/2011| CASH BALANCE | | | 5000
1/1/2011| SALES | 2500 | | 7500
2/1/2011| PURCHASE | | 3000 | 4500
2/1/2011| RENT | | 4000 | 500
2/1/2011| SALES | 5000 | | 5500
I can use CASHBOOKTABLEADAPTER.INSERT(...) to insert appropriately, but my problem is how do I update the BALANCE column?
See this article by Alexander Kuznetsov
Denormalizing to enforce business rules: Running Totals
You can try an insert with a subquery, something like following:
INSERT INTO CASHBOOK ( DESCRIPTION, DEBIT, BALANCE )
'asdf', 2500, SELECT TOP(1) BALANCE FROM CASHBOOK + 2500
It's a bit heavy handed, but here's a way to update the full table with balance information.
update
a
set
a.Balance = (
select sum(isnull(x.debit, 0.0) - isnull(x.credit, 0.0))
from cashbook x
where x.Date < a.Date
or (x.Date = a.Date and x.ID <= a.ID)
) + (
select top 1 y.Balance
from cashbook y
where y.debit is null
and y.credit is null
order by y.ID
)
from
cashbook a
Now that's useful only if you HAVE to have the balance in the table. A more appropriate solution might be to create a UDF that encompasses this logic and call that to calculate the balance field for a specific row only when you need it. It really all depends on your usage.
create function dbo.GetBalance(#id int) returns decimal(12, 2) as
begin
declare #result decimal(12, 2) = 0.0
select
#result = (
select sum(isnull(x.debit, 0.0) - isnull(x.credit, 0.0))
from cashbook x
where x.Date < a.Date
or (x.Date = a.Date and x.ID <= a.ID)
) + (
select top 1 y.Balance
from cashbook y
where y.debit is null
and y.credit is null
order by y.ID
)
from
cashback a
where
a.ID = #id
return #result
end
Why do you need to? This is something that should be calculated as a reporting / viewing function. I would suggest either creating a view with a running total column (various ways to achieve this).
Alternatively if you're viewing this in VB.Net calculate it in your app.
I agree with Joel, you should be calculating this at runtime, not storing the running totals in the database. Here's an example of how to figure out the running totals using a recursive cte in sql server:
declare #values table (ID int identity(1,1), Value decimal(4,2))
declare #i int
insert into #values values (1.00)
insert into #values values (2.00)
insert into #values values (3.00)
insert into #values values (4.00)
insert into #values values (5.00)
insert into #values values (6.00)
select #i=min(ID) from #values
;with a as
(
select ID, Value, Value as RunningTotal
from #values
where ID=#i
union all
select b.ID, b.Value, cast(b.Value + a.RunningTotal as decimal(4,2)) as RunningTotal
from #values b
inner join a
on b.ID=a.ID+1
)
select * from a
here's a blog on recursive queries: Recursive CTEs
Also here's a lengthy discusson about running totals.
One potential problem with recursive CTEs is the maximum depth limit of 32767, which can be prohibitive in a production environment.
In this solution you add an id column that is ordinal to the transaction sequence and then update the balance column in place.
declare #t table(id int identity(1,1) not null
, [DATE] date not null
, [DESCRIPTION] varchar(80) null
, [DEBIT] money not null default(0)
, [CREDIT] money not null default(0)
, [BALANCE] money not null default(0)
);
declare #bal money=0;
insert into #t([DATE],[DESCRIPTION],[DEBIT],[CREDIT],[BALANCE])
select '1/1/2011','CASH BALANCE',0,0,5000 UNION ALL
select '1/1/2011','SALES',2500,0,0 UNION ALL
select '2/1/2011','PURCHASE',0,3000,0 UNION ALL
select '2/1/2011','RENT',0,4000,0 UNION ALL
select '2/1/2011','SALES',5000,0,0;
set #bal=(select top 1 [BALANCE] from #t order by id); /* opening balance is stored but not computed, so we simply look it up here. */
update t
set #bal=t.[BALANCE]=(t.[DEBIT]-t.[CREDIT])+#bal
output
inserted.*
from #t t
left join #t t0 on t0.id+1=t.id; /*should order by id by default, but to be safe we force the issue here. */