Subnet (or CIDR) IP control at T-SQL - tsql

I don't know how to exactly explain, but there is a problem about selecting and comparing query IP subnet. For example, there is a list for IP address and I have another CIDR/subnet mask list (X.X.X.0/24 etc). How can I learn that each IP address in first list is in CIDR/subnet mask list via T-SQL?
For example:
IP: 172.28.112.23 -> false
IP: 172.28.111.33 -> true
IP List Output:
SubNet Output:

You want to do exactly what a computer would do to determine if an ip address is in a subnet- ie:
1) convert the network address, subnet mask and test address to binary.
2) Check if (Network Address & Subnet Mask) = (Test Address & Subnet mask)
(& represents bitwise AND)
If this comparison is true the test address is within the subnet
The key to understanding this is to realise that IP addresses (and subnet masks) are just 32 bit numbers.
A bitwise and between 2 32 bit numbers creates a new 32 bit number with a 1 in the position where there was a 1 in both of the 2 numbers being compared, and a 0 otherwise.
EG: 1010 & 1100 = 1000 because the first digit is 1 in both numbers (yielding a 1 in the result for the first digit), but the 2nd 3rd and 4th digits are not (so give 0 in the result for the 2nd 3rd and 4th digits).
SQL Server cannot do a bitwise and between 2 binary numbers unfortunately, but it works fine between decimal representations (ie when converted to BIGINT datatype).
Therefore I'd propose you create a function that converts your IP addresses to BIGINT datatype firstly
CREATE FUNCTION dbo.fnIPtoBigInt
(
#Ipaddress NVARCHAR(15) -- should be in the form '123.123.123.123'
)
RETURNS BIGINT
AS
BEGIN
DECLARE #part1 AS NVARCHAR(3)
DECLARE #part2 AS NVARCHAR(3)
DECLARE #part3 AS NVARCHAR(3)
DECLARE #part4 AS NVARCHAR(3)
SELECT #part1 = LEFT(#Ipaddress, CHARINDEX('.',#Ipaddress) - 1)
SELECT #Ipaddress = SUBSTRING(#Ipaddress, LEN(#part1) + 2, 15)
SELECT #part2 = LEFT(#Ipaddress, CHARINDEX('.',#Ipaddress) - 1)
SELECT #Ipaddress = SUBSTRING(#Ipaddress, LEN(#part2) + 2, 15)
SELECT #part3 = LEFT(#Ipaddress, CHARINDEX('.',#Ipaddress) - 1)
SELECT #part4 = SUBSTRING(#Ipaddress, LEN(#part3) + 2, 15)
DECLARE #ipAsBigInt AS BIGINT
SELECT #ipAsBigInt =
(16777216 * (CAST(#part1 AS BIGINT)))
+ (65536 * (CAST(#part2 AS BIGINT)))
+ (256 * (CAST(#part3 AS BIGINT)))
+ (CAST(#part4 AS BIGINT))
RETURN #ipAsBigInt
END
GO
Then you can easily implement a function to test if an address is in a subnet:
CREATE FUNCTION dbo.fnIsIpaddressInSubnet
(
#networkAddress NVARCHAR(15), -- 'eg: '192.168.0.0'
#subnetMask NVARCHAR(15), -- 'eg: '255.255.255.0' for '/24'
#testAddress NVARCHAR(15) -- 'eg: '192.168.0.1'
)
RETURNS BIT AS
BEGIN
RETURN CASE WHEN (dbo.fnIPtoBigInt(#networkAddress) & dbo.fnIPtoBigInt(#subnetMask))
= (dbo.fnIPtoBigInt(#testAddress) & dbo.fnIPtoBigInt(#subnetMask))
THEN 1 ELSE 0 END
END
To make this a bit easier for you you'll probably want a function that can convert '/24' to a BigInt too.
'/24' is a shorthand way of writing 255.255.255.0 - ie a 32bit number with the first 24bits set to 1 (and the remaining 8 bits set to 0)
CREATE FUNCTION dbo.fnSubnetBitstoBigInt
(
#SubnetBits TINYINT -- max = 32
)
RETURNS BIGINT
AS
BEGIN
DECLARE #multiplier AS BIGINT = 2147483648
DECLARE #ipAsBigInt AS BIGINT = 0
DECLARE #bitIndex TINYINT = 1
WHILE #bitIndex <= #SubnetBits
BEGIN
SELECT #ipAsBigInt = #ipAsBigInt + #multiplier
SELECT #multiplier = #multiplier / 2
SELECT #bitIndex = #bitIndex + 1
END
RETURN #ipAsBigInt
END
GO
If you create the following additional function the conversion becomes easy
CREATE FUNCTION dbo.fnIsIpaddressInSubnetShortHand
(
#network NVARCHAR(18), -- 'eg: '192.168.0.0/24'
#testAddress NVARCHAR(15) -- 'eg: '192.168.0.1'
)
RETURNS BIT AS
BEGIN
DECLARE #networkAddress NVARCHAR(15)
DECLARE #subnetBits TINYINT
SELECT #networkAddress = LEFT(#network, CHARINDEX('/', #network) - 1)
SELECT #subnetBits = CAST(SUBSTRING(#network, LEN(#networkAddress) + 2, 2) AS TINYINT)
RETURN CASE WHEN (dbo.fnIPtoBigInt(#networkAddress) & dbo.fnSubnetBitstoBigInt(#subnetBits))
= (dbo.fnIPtoBigInt(#testAddress) & dbo.fnSubnetBitstoBigInt(#subnetBits))
THEN 1 ELSE 0 END
END
i.e.
SELECT dbo.fnIsIpaddressInSubnetShorthand('192.168.2.0/24','192.168.3.91') -- returns 0
SELECT dbo.fnIsIpaddressInSubnetShorthand('192.168.2.0/24','192.168.2.91') -- returns 1

This is not an answer in itself, but a way to make one of the functions in James S answer easier to read and possibly more efficient.
SQL Server has a function to handle getting parts from database object names. Those names are 4 parts [Server].[Database].[Schema].[Object]. So the following allows you to get the schema name. The index works from the right
SELECT PARSENAME('[myServer].[master].[sys].[objects]', 2)
There's nothing to say you can't use that for an IP address. And as it's so fundamental to how SQL operates I assume it has been hella optimised.
CREATE FUNCTION dbo.fnIPtoBigInt
(
#Ipaddress NVARCHAR(15) -- should be in the form '123.123.123.123'
)
RETURNS BIGINT
AS
BEGIN
DECLARE #ipAsBigInt AS BIGINT
SELECT #ipAsBigInt =
(16777216 * (CAST(PARSENAME(#Ipaddress, 4) AS BIGINT)))
+ (65536 * (CAST(PARSENAME(#Ipaddress, 3) AS BIGINT)))
+ (256 * (CAST(PARSENAME(#Ipaddress, 2) AS BIGINT)))
+ (CAST(PARSENAME(#Ipaddress, 1) AS BIGINT))
RETURN #ipAsBigInt
END
GO

Complete solution
CREATE
OR ALTER FUNCTION dbo.IPv4SubnetContainsIPAddress (
#net AS VARCHAR(15),
#mask AS VARCHAR(15),
#ip AS VARCHAR(15)
) RETURNS tinyint AS BEGIN DECLARE #result AS tinyint IF LEN(#mask) <= 2
SELECT
#mask = m
FROM
(
VALUES
(0, '0.0.0.0'),
(1, '128.0.0.0'),
(2, '192.0.0.0'),
(3, '224.0.0.0'),
(4, '240.0.0.0'),
(5, '248.0.0.0'),
(6, '252.0.0.0'),
(7, '254.0.0.0'),
(8, '255.0.0.0'),
(9, '255.128.0.0'),
(10, '255.192.0.0'),
(11, '255.224.0.0'),
(12, '255.240.0.0'),
(13, '255.248.0.0'),
(14, '255.252.0.0'),
(15, '255.254.0.0'),
(16, '255.255.0.0'),
(17, '255.255.128.0'),
(18, '255.255.192.0'),
(19, '255.255.224.0'),
(20, '255.255.240.0'),
(21, '255.255.248.0'),
(22, '255.255.252.0'),
(23, '255.255.254.0'),
(24, '255.255.255.0'),
(25, '255.255.255.128'),
(26, '255.255.255.192'),
(27, '255.255.255.224'),
(28, '255.255.255.240'),
(29, '255.255.255.248'),
(30, '255.255.255.252'),
(31, '255.255.255.254'),
(32, '255.255.255.255')
) AS o (i, m)
WHERE
i = #mask
SELECT
#result = IIF(Count(*) = 4, 1, 0)
FROM
(
SELECT
*,
IIF(
o_ip BETWEEN o_subnet
AND o_broadcast,
1,
0
) AS eq
FROM
(
SELECT
*,
o_net & o_mask AS o_subnet,
o_net | (255 - o_mask) AS o_broadcast
FROM
(
SELECT
o_net,
o_mask,
o_ip
FROM
(
VALUES
(1, CAST(PARSENAME(#net, 4) AS INTEGER)),
(2, CAST(PARSENAME(#net, 3) AS INTEGER)),
(3, CAST(PARSENAME(#net, 2) AS INTEGER)),
(4, CAST(PARSENAME(#net, 1) AS INTEGER))
) AS c1 (i, o_net)
LEFT JOIN (
SELECT
i,
o_mask
FROM
(
VALUES
(1, CAST(PARSENAME(#mask, 4) AS INTEGER)),
(2, CAST(PARSENAME(#mask, 3) AS INTEGER)),
(3, CAST(PARSENAME(#mask, 2) AS INTEGER)),
(4, CAST(PARSENAME(#mask, 1) AS INTEGER))
) AS c2 (i, o_mask)
) AS c2 ON c1.i = c2.i
LEFT JOIN (
SELECT
i,
o_ip
FROM
(
VALUES
(1, CAST(PARSENAME(#ip, 4) AS INTEGER)),
(2, CAST(PARSENAME(#ip, 3) AS INTEGER)),
(3, CAST(PARSENAME(#ip, 2) AS INTEGER)),
(4, CAST(PARSENAME(#ip, 1) AS INTEGER))
) AS c3 (i, o_ip)
) AS c3 ON c1.i = c3.i
) AS t
) AS t
) AS t
WHERE
eq = 1 RETURN #result END
GO
SELECT
dbo.IPv4SubnetContainsIPAddress('192.168.64.0', '255.255.224.0', '192.168.40.1') -- returns 0
SELECT
dbo.IPv4SubnetContainsIPAddress('192.168.64.0', '19', '192.168.40.1') -- returns 0
SELECT
dbo.IPv4SubnetContainsIPAddress('192.168.64.0', '255.255.192.0', '192.168.80.1') -- returns 1
SELECT
dbo.IPv4SubnetContainsIPAddress('192.168.64.0', '18', '192.168.80.1') -- returns 1

Related

(Postgres) Query in a tree table in ascending and descending mode

I'm having some issues with two queries to search in a "tree" table.
So, my table is represented by the following code, and it has one only direction. However, I need to get data in both directions, ascending and descending mode.
create table graph_examle (input int null, output int );
insert into graph_examle (input, output) values
(null, 1),
(1, 2),
(2, 3 ),
(3, 4 ),
(null, 7 ),
(7,8),
(8, 4 ),
(null, 10 ),
(10, 11 ),
(11, 4),
(3, 15),
(25, 15),
(26, 15),
(15, 4 );
The ascending query has some issues. If I search by id 1, I'm expecting to see the relations:
1, 1->2, 1->2->3, 1->2->3->4, but the results are:
WITH recursive cte (initial_id, level, path, loop, input, output) AS
(
SELECT input, 1, ':' ||input || ':' , 0, input, output
FROM graph_examle WHERE input = 1
UNION ALL
SELECT
c.initial_id,
c.level + 1,
c.path ||ur.input|| ':' ,
CASE WHEN c.path LIKE '%:' ||ur.input || ':%' THEN 1 ELSE 0 END,
ur.*
FROM graph_examle ur
INNER JOIN cte c ON c.output = ur.input AND c.loop = 0
)
SELECT *
FROM cte
ORDER BY initial_id, level;
The descending query does not work as expected. If I search by id 4, I'm expecting to see the relations:
4, 4->3, 4->3->2, 4->3->2->1
4->8, 4->8->7
4->11,4->11>10
4->15, (...)
But I'm only getting:
WITH RECURSIVE cte (input, output, level, real_parent_id, path) AS
(
SELECT
ur.input, ur.input, 1, output, ( ur.input|| ' -> ' || ur.output)
FROM graph_examle ur
WHERE ur.output = 4
UNION ALL
SELECT
ur_cte.input, ur.input, level + 1, ur.output, (ur_cte.path || '->' || ur.output)
FROM cte ur_cte
INNER JOIN graph_examle ur on ur.input = ur_cte.real_parent_id
)
SELECT *
FROM cte
ORDER BY path
Note that in my queries I'm trying to solve circular dependencies
The ascending query sounds good ... maybe you can concatenate the path and output columns.
For the descending query, you can try this :
WITH RECURSIVE cte (input, output, level, path, loop) AS
(
SELECT
ur.input, ur.output, 1, ( ur.output|| ' -> ' || ur.input), 0
FROM graph_examle ur
WHERE ur.output = 4
UNION ALL
SELECT
ur.input, ur_cte.output, level + 1, (ur_cte.path || '->' || ur.input),
CASE WHEN ur_cte.path LIKE '%->' || ur.input THEN 1 ELSE 0 END
FROM cte ur_cte
INNER JOIN graph_examle ur on ur.output = ur_cte.input
WHERE ur_cte.loop = 0
AND ur.input IS NOT NULL
)
SELECT *
FROM cte
ORDER BY path
see dbfiddle

TSQL - Problem converting and updating a float column

I have tried many ways but could not find the answer. My problem is:
there is Table ORG_DATA_AS_VARCHAR with a columnFLOATNMBRS (varchar)
FLOATNMBRS
--------------------------
0
0
*0,25 /*Yeah, there is a star in data ... bad data quality ....*/
*0,31
0
Now, my aim is to convert this strings to a float and update these new float values to a new (existing) table CONVERTED_DATA:
FLOATNMBRS (float)
--------------------------
0
0
0.25
0.31
0
...
What I have tried:
UPDATE CONVERTED_DATA
SET
FLOATNMBRS = b.newValue
FROM
(
Select convert (float, replace(replace(FLOATNMBRS, '*', ''),',','.')) as newValue from
ORG_DATA_AS_VARCHAR
) b
or
Replacing and converting it and create a #Temp Table with the new Values and Update CONVERTED_DATA with values from #Temp.
but everytime I ended up like:
FLOATNMBRS (float)
--------------------------
0
0
0
0
0
All values were updated as 0.
When I tried:
Select convert (float, replace(replace(FLOATNMBRS, '*', ''),',','.')) as newValue from
ORG_DATA_AS_VARCHAR
the result is correct. Even when I copy the value to #Temp. All values are correct.
Does someone know what I m doing wrong ???
You are possibly not matching the records in the 2 tables. It is not clear from your example how the rows are identified (what's the key).
Assuming you have the same ID in ORG_DATA_AS_VARCHAR and CONVERTED_DATA tables, this works:
create table #ORG_DATA_AS_VARCHAR (ID int, floatnmbrs_varchar varchar(128))
create table #CONVERTED_DATA (ID int, floatnmbrs float)
go
insert into #ORG_DATA_AS_VARCHAR (ID, floatnmbrs_varchar)
values (1, '0'), (2, '0'), (3, '*0,25'), (4, '*0.31'), (5, '0')
insert into #CONVERTED_DATA (ID, floatnmbrs)
values (1, 0), (2, 0), (3, 0), (4, 0), (5, 0)
go
update #CONVERTED_DATA
set floatnmbrs = x.converted
from (
select ID, converted = convert(float, replace(replace(floatnmbrs_varchar, '*', ''), ',', '.'))
from #ORG_DATA_AS_VARCHAR
) as x
where x.ID = #CONVERTED_DATA.ID
select * from #CONVERTED_DATA
go
drop table #ORG_DATA_AS_VARCHAR
drop table #CONVERTED_DATA
go

Converting a table with a key and comment field into a key and row for every word in the column field

I have a table with unstructured data I am trying to analyze to try to build a relational lookup. I do not have use of word cloud software.
I really have no idea how to solve this problem. Searching for solutions has lead me to tools that might do this for me that cost money, not coded solutions.
Basically my data looks like this:
CK1 CK2 Comment
--------------------------------------------------------------
1 A This is a comment.
2 A Another comment here.
And this is what I need to create:
CK1 CK2 Words
--------------------------------------------------------------
1 A This
1 A is
1 A a
1 A comment.
2 A Another
2 A comment
2 A here.
What you are trying to do is tokenize a string using a space as a Delimiter. In the SQL world people often refer to functions that do this as a "Splitter". The potential pitfall of using a splitter for this type of thing is how words can be separated by multiple spaces, tabs, CHAR(10)'s, CHAR(13)'s, CHAR()'s, etc. Poor grammar, such as not adding a space after a period results in this:
" End of sentence.Next sentence"
sentence.Next is returned as a word.
The way I like to tokenize human text is to:
Replace any text that isn't a character with a space
Replace duplicate spaces
Trim the string
Split the newly transformed string using a space as the delimiter.
Below is my solution followed by the DDL to create the functions used.
-- Sample Data
DECLARE #yourtable TABLE (CK1 INT, CK2 CHAR(1), Comment VARCHAR(8000));
INSERT #yourtable (CK1, CK2, Comment)
VALUES
(1,'A','This is a typical comment...Follewed by another...'),
(2,'A','This comment has double spaces and tabs and even carriage
returns!');
-- Solution
SELECT t.CK1, t.CK2, split.itemNumber, split.itemIndex, split.itemLength, split.item
FROM #yourtable AS t
CROSS APPLY samd.patReplace(t.Comment,'[^a-zA-Z ]',' ') AS c1
CROSS APPLY samd.removeDupChar8K(c1.newString,' ') AS c2
CROSS APPLY samd.delimitedSplitAB8K(LTRIM(RTRIM(c2.NewString)),' ') AS split;
Results (truncated for brevity):
CK1 CK2 itemNumber itemIndex itemLength item
----------- ---- -------------------- ----------- ----------- --------------
1 A 1 1 4 This
1 A 2 6 2 is
1 A 3 9 1 a
1 A 4 11 7 typical
1 A 5 19 7 comment
...
2 A 1 1 4 This
2 A 2 6 7 comment
2 A 3 14 3 has
2 A 4 18 6 double
...
Note that the splitter I'm using is based of Jeff Moden's Delimited Split8K with a couple tweeks.
Functions used:
CREATE FUNCTION dbo.rangeAB
(
#low bigint,
#high bigint,
#gap bigint,
#row1 bit
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
WITH L1(N) AS
(
SELECT 1
FROM (VALUES
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),
(0),(0)) T(N) -- 90 values
),
L2(N) AS (SELECT 1 FROM L1 a CROSS JOIN L1 b CROSS JOIN L1 c),
iTally AS (SELECT rn = ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM L2 a CROSS JOIN L2 b)
SELECT r.RN, r.OP, r.N1, r.N2
FROM
(
SELECT
RN = 0,
OP = (#high-#low)/#gap,
N1 = #low,
N2 = #gap+#low
WHERE #row1 = 0
UNION ALL -- COALESCE required in the TOP statement below for error handling purposes
SELECT TOP (ABS((COALESCE(#high,0)-COALESCE(#low,0))/COALESCE(#gap,0)+COALESCE(#row1,1)))
RN = i.rn,
OP = (#high-#low)/#gap+(2*#row1)-i.rn,
N1 = (i.rn-#row1)*#gap+#low,
N2 = (i.rn-(#row1-1))*#gap+#low
FROM iTally AS i
ORDER BY i.rn
) AS r
WHERE #high&#low&#gap&#row1 IS NOT NULL AND #high >= #low AND #gap > 0;
GO
CREATE FUNCTION samd.NGrams8k
(
#string VARCHAR(8000), -- Input string
#N INT -- requested token size
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT
position = r.RN,
token = SUBSTRING(#string, CHECKSUM(r.RN), #N)
FROM dbo.rangeAB(1, LEN(#string)+1-#N,1,1) AS r
WHERE #N > 0 AND #N <= LEN(#string);
GO
CREATE FUNCTION samd.patReplace8K
(
#string VARCHAR(8000),
#pattern VARCHAR(50),
#replace VARCHAR(20)
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT newString =
(
SELECT CASE WHEN #string = CAST('' AS VARCHAR(8000)) THEN CAST('' AS VARCHAR(8000))
WHEN #pattern+#replace+#string IS NOT NULL THEN
CASE WHEN PATINDEX(#pattern,token COLLATE Latin1_General_BIN)=0
THEN ng.token ELSE #replace END END
FROM samd.NGrams8K(#string, 1) AS ng
ORDER BY ng.position
FOR XML PATH(''),TYPE
).value('text()[1]', 'VARCHAR(8000)');
GO
CREATE FUNCTION samd.delimitedSplitAB8K
(
#string VARCHAR(8000), -- input string
#delimiter CHAR(1) -- delimiter
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT
itemNumber = ROW_NUMBER() OVER (ORDER BY d.p),
itemIndex = CHECKSUM(ISNULL(NULLIF(d.p+1, 0),1)),
itemLength = CHECKSUM(item.ln),
item = SUBSTRING(#string, d.p+1, item.ln)
FROM (VALUES (DATALENGTH(#string))) AS l(s) -- length of the string
CROSS APPLY
(
SELECT 0 UNION ALL -- for handling leading delimiters
SELECT ng.position
FROM samd.NGrams8K(#string, 1) AS ng
WHERE token = #delimiter
) AS d(p) -- delimiter.position
CROSS APPLY (VALUES( --LEAD(d.p, 1, l.s+l.d) OVER (ORDER BY d.p) - (d.p+l.d)
ISNULL(NULLIF(CHARINDEX(#delimiter,#string,d.p+1),0)-(d.p+1), l.s-d.p))) AS item(ln);
GO
CREATE FUNCTION dbo.RemoveDupChar8K(#string varchar(8000), #char char(1))
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT NewString =
replace(replace(replace(replace(replace(replace(replace(
#string COLLATE LATIN1_GENERAL_BIN,
replicate(#char,33), #char), --33
replicate(#char,17), #char), --17
replicate(#char,9 ), #char), -- 9
replicate(#char,5 ), #char), -- 5
replicate(#char,3 ), #char), -- 3
replicate(#char,2 ), #char), -- 2
replicate(#char,2 ), #char); -- 2
GO
1) If we are using SQL Server 2016 and above then we should probably
use the built-in function STRING_SPLIT
-- SQL 2016and above
DECLARE #txt NVARCHAR(100) = N'This is a comment.'
select [value] from STRING_SPLIT(#txt, ' ')
2) Only if 1 does not fit, then if the number of separation (the space in our case) is less then 3 which fit your sample data, then we should probably use PARSENAME
-- BEFORE SQL 2016 if we have less than 4 parts
DECLARE #txt NVARCHAR(100) = N'This is a comment.'
DECLARE #Temp NVARCHAR(200) = REPLACE (#txt,'.','#')
SELECT t FROM (VALUES(1),(2),(3),(4))T1(n)
CROSS APPLY (SELECT REPLACE(PARSENAME(REPLACE(#Temp,' ','.'),T1.n), '#','.'))T2(t)
3) Only if the 1 and 2 does not fit, then we should use SQLCLR function
http://dataeducation.com/sqlclr-string-splitting-part-2-even-faster-even-more-scalable/
4) Only if we cannot use 1,2 and we cannot use SQLCLR (which implies a real problematic administration and has nothing with security since you can have all the SQLCLR function in a read-only database for the use of all users, as I explain in my lectures), then you can use T-SQL and create UDF.
https://sqlperformance.com/2012/07/t-sql-queries/split-strings

PostgreSQL, Variables

I am using PostgreSQL through Npgsql driver for windows/NET and I see that it is possible to use PL/pgSQL language through it.
So that way I can make use of variables for my calculation scripts which may look like in this example:
DO $$
DECLARE
tlist text='mylistofbills';
tcontent text='mycontentofbills';
BEGIN
CREATE TEMP TABLE tlist
(billno integer, bdate timestamp, rebate double precision)
ON COMMIT DROP;
INSERT INTO tlist
VALUES (1, '10.01.2017. 10:14:56', 10),
(2, '10.01.2017. 11:02:13', 5),
(3, '10.01.2017. 11:45:22', 0),
(4, '10.01.2017. 12:01:01', 6);
CREATE TEMP TABLE tcontent
(billno integer, rowno integer, price double precision, tax double precision)
ON COMMIT DROP;
INSERT INTO tcontent
VALUES (1, 1, 100, 19),
(1, 2, 30, 0),
(2, 1, 20, 19),
(3, 1, 18, 19),
(4, 1, 43, 0);
END $$;
SELECT s.price,
l.rebate,
s.price/100*l.rebate AS valrebate,
s.price-(s.price/100*l.rebate) AS worebate,
((s.price-(s.price/100*l.rebate))/100)*s.tax AS valtax,
s.price-(s.price/100*l.rebate)+(((s.price-(s.price/100*l.rebate))/100)*s.tax) AS finalprice
FROM tlist l, tcontent s
WHERE l.billno=s.billno;
Example is simplified (from real situation) and is suitable for pasting into PgAdmin's SQL editor.
So, now is question: Can I somehow in the body of those code, without adding new functions to server use formulas for writing more elegant and readable code?
If I would be able to add simple formulas like:
rebatec=s.price/100*l.rebate
priceworebate=s.price-rebatec
Then my code may look more readable and less error prone.
Like this:
SELECT s.price,
l.rebate,
rebatec AS valrebate,
priceworebate AS worebate,
(priceworebate/100)*s.tax AS valtax,
priceworebate+((priceworebate/100)*s.tax) AS finalprice
FROM tlist l, tcontent s
WHERE l.billno=s.billno;
If that may be possible where and how to put this formulas so it can be used in my last SELECT code?
SOLUTION:
Based on #Clodoaldo's answer which give something new to me I find a solution which I am able to understand:
SELECT s.price,
l.rebate,
rebatec AS valrebate,
priceworebate AS worebate,
priceworebate/100*s.tax AS valtax,
priceworebate+priceworebate/100*s.tax AS finalprice
FROM tlist l, tcontent s, LATERAL
(SELECT s.price/100*l.rebate AS rebatec,
s.price-s.price/100* l.rebate AS priceworebate
)sub
WHERE l.billno=s.billno;
It works and I hope it is technically correct.
Use lateral:
The LATERAL key word can precede a sub-SELECT FROM item. This allows the sub-SELECT to refer to columns of FROM items that appear before it in the FROM list.
select
s.price,
l.rebate,
rebatec as valrebate,
priceworebate as worebate,
priceworebate / 100 * s.tax as valtax,
priceworebate + priceworebate / 100 * s.tax as finalprice
from
tlist l
inner join
tcontent s using (billno)
cross join lateral (
select
s.price / 100 * l.rebate as rebatec,
s.price - s.price / 100 * l.rebate as priceworebate
) cjl
Use the modern join syntax.
You could use a subquery to define those variables:
select var1 * col3
from (
select col1 / col2 as var1
, *
from YourTable
) sub
Or alternatively a common table expression:
with cte as
(
select col1 / col2 as var1
, *
from YourTable
)
select var1 * col3
from cte

Split string with a separator and set each part in variables in TSQL

I need to split some strings with this format:
V_9_0_2_2_70_0_0_3_B
The separator is '_'.
I want that each value is stored in my variables
DECLARE #w_grup1 char(1),
#w_grup2 char(1),
#w_grup3 varchar(10),
#w_grup4 char(1),
#w_grup5 char(1),
#w_grup6 varchar(10),
#w_grup7 char(1),
#w_grup8 varchar(10),
#w_grup9 char(1),
#w_grup10 char(1)
How do I do that? Some suggestion?
I figured your best bet is a recursive CTE. Note: I didn't load the data into a table but I figure you can do that easily enough from my results. If you need anything else, let me know.
DECLARE #YourTable table (ID INT IDENTITY(1,1), String varchar(200))
INSERT #YourTable(String)
VALUES ('V_9_0_2_2_70_0_0_3_B'),
('ABC_01_23_45_67_89_10_11_12_XYZ');
WITH SplitString AS
(
SELECT ID,
LEFT(String,CHARINDEX('_',String)-1) AS Part,
RIGHT(String,LEN(String)-CHARINDEX('_',String)) AS Remainder,
1 AS RecursionCount
FROM #YourTable
WHERE String IS NOT NULL AND CHARINDEX('_',String) > 0
UNION ALL
SELECT ID,
LEFT(Remainder,CHARINDEX('_',Remainder)-1),
RIGHT(Remainder,LEN(Remainder)-CHARINDEX('_',Remainder)),
RecursionCount + 1
FROM SplitString
WHERE Remainder IS NOT NULL AND CHARINDEX('_',Remainder) > 0
UNION ALL
SELECT ID,
Remainder,
null,
recursionCount + 1
FROM SplitString
WHERE Remainder IS NOT NULL AND CHARINDEX('_',Remainder) = 0
)
SELECT *
FROM
(
SELECT CONCAT('w_grup',RecursionCount) w_grups,Part,ID
FROM SplitString
) A
PIVOT
(
MAX(Part) FOR w_grups IN (w_grup1,w_grup2,w_grup3,w_grup4,w_grup5,w_grup6,w_grup7,w_grup8,w_grup9,w_grup10)
) pvt
Results:
ID w_grup1 w_grup2 w_grup3 w_grup4 w_grup5 w_grup6 w_grup7 w_grup8 w_grup9 w_grup10
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 V 9 0 2 2 70 0 0 3 B
2 ABC 01 23 45 67 89 10 11 12 XYZ
So, something like this?
with split as
(
select
item = cast('' as varchar(max)),
source = cast('V_9_0_2_2_70_0_0_3_B' as varchar(max))
union all
select
item = substring(source, 1, charindex('_', source)),
source = substring(source, charindex('_', source) + 1, 10000)
from split
where source > ''
)
select substring(item, 1, charindex('_', item) -1)
from split
where item > ''
from this question