using charindex in a substring to trim a string - sql

There is a column name from which I want to use to make a new column.
example:
name
asd_abceur1mz_a
asd_fxasdrasdusd3mz_a
asd_abceur10yz_a
asd_fxasdrasdusd15yz_a
The length of the column is not fixed so I assumed i have to use charindex to have a reference point from which I could trim.
What i want: at the end there is always z_a, and i need to place in a separate column the left part from z_a like this:
nameNew
eur1m
usd3m
eur10y
usd15y
The problem is that the number (in this example 1, 3, 10, 15) has 1 or two digits. I need to extract the information from name to nameNew.
After that i was thinking to make it easier to read and to output it like this:
eur_1m
usd_3m
eur_10y
usd_15y
I tried using a combination of substring and charindex, but so far without success.
SELECT *
, SUBSTRING(name, 1, ( CHARINDEX('z_a', NAME) - 1 )) AS nameNew
FROM myTable
This is for the first step, trimming the string, for the 2nd step (making it easier to read) I don't know how to target the digit and place an _.
Any help would be appreciated. Using sql server 2012
edit:
First of all thank you for your time and solutions. But your queries more or less even if they are working for 1 or 2 digits have the same problem. Consider this situation:
name
ab_dertEUR03EUR10YZ_A
if eur is two times in the string, then how can I eliminate this? Sorry for not includding this in my original post but i forgot that situation is possible and now that's a problem.
edit:
test your queries here, on this example:
http://www.sqlfiddle.com/#!3/21610/1
Please note that at the end it can be any combination of 1 or 2 digits and the letter y or m.
Ex: ab_rtgtEUR03EUR2YZ_A , ab_rtgtEUR03EUR2mZ_A, ab_rtgtEUR03EUR20YZ_A, ab_rtgtEUR03EUR20mZ_A
Some values for testing:
('ex_CHFCHF01CHF10YZ_A'), ('ab_rtgtEUR03EUR2YZ_A'), ('RON_asdRON2MZ_A'),
('tg_USDUSD04USD5YZ_A');
My understanding of your queries is that they perform something simillar to this (or at least they should)
ex_CHFCHF01CHF10YZ_A -> ex_CHFCHF01CHF10Y -> Y01FHC10FHCFHC -> Y01FHC -> CHF01Y -> CHF_01Y
RON_asdRON2MZ_A -> RON_asdRON2M -> M2NORdsa_ron -> M2NOR -> RON2M -> RON_2M

This works for one or two digits:
stuff(case
when name like '%[0-9][0-9]_z[_]a'
then left(right(name, 9), 6)
when name like '%[0-9]_z[_]a'
then left(right(name, 8), 5)
end, 4, 0, '_')

You can use a combination of substring , reverse and charindex.
SQL Fiddle
select substring(namenew,1,3) + '_' + substring(namenew, 4, len(namenew))
from (
select
case when name like '%[0-9][0-9]_z[_]a' then
reverse(substring(reverse(name), charindex('a_z',reverse(name)) + 3, 6))
when name like '%[0-9]_z[_]a' then
reverse(substring(reverse(name), charindex('a_z',reverse(name)) + 3, 5))
end as namenew
from myTable
) t

Try it like this:
declare #tbl TABLE(name VARCHAR(100));
insert into #tbl VALUES
('asd_abceur1mz_a')
,('asd_fxasdrasdusd3mz_a')
,('asd_abceur10yz_a')
,('asd_fxasdrasdusd15yz_a')
,('ab_dertEUR03EUR10YZ_A');
WITH CutOfThreeAtTheEnd AS
(
SELECT LEFT(name,LEN(name)-3) AS nameNew
FROM #tbl
)
,Max6CharsFromEnd AS
(
SELECT RIGHT(nameNew,6) AS nameNew
FROM CutOfThreeAtTheEnd
)
SELECT nameNew
,FirstNumber.Position
,Parts.*
,Parts.FrontPart + '_' + Parts.BackPart AS FinalString
FROM Max6CharsFromEnd
CROSS APPLY
(
SELECT MIN(x)
FROM
(
SELECT CHARINDEX('0',nameNew,1) AS x
UNION SELECT CHARINDEX('1',nameNew,1)
UNION SELECT CHARINDEX('2',nameNew,1)
UNION SELECT CHARINDEX('3',nameNew,1)
UNION SELECT CHARINDEX('4',nameNew,1)
UNION SELECT CHARINDEX('5',nameNew,1)
UNION SELECT CHARINDEX('6',nameNew,1)
UNION SELECT CHARINDEX('7',nameNew,1)
UNION SELECT CHARINDEX('8',nameNew,1)
UNION SELECT CHARINDEX('9',nameNew,1)
) AS tbl
WHERE x>0
) AS FirstNumber(Position)
CROSS APPLY(SELECT SUBSTRING(nameNew,FirstNumber.Position,1000) AS BackPart
,SUBSTRING(nameNew,FirstNumber.Position-3,3) AS FrontPart) AS Parts
this is the result:
nameNew Position BackPart FrontPart FinalString
ceur1m 5 1m eur eur_1m
dusd3m 5 3m usd usd_3m
eur10y 4 10y eur eur_10y
usd15y 4 15y usd usd_15y
EUR10Y 4 10Y EUR EUR_10Y

Related

How to extract this specific data from a particular column in SQL Server?

I have column with below data:
Change
18 MCO-005329
A ECO-12239
0 ECO-25126
X1 ECO-05963
NA MCO-003778
C ECO-08399
MCO-003759
ECO-00643217
NULL
I want to extract the output like below:
MCO-005329
ECO-12239
ECO-25126
ECO-05963
MCO-003778
ECO-08399
MCO-003759
ECO-00643217
I have implemented the code like below:
select DISTINCT change,
case when change like 'MCO%' THEN change when change like 'ECO-%' THEN change
when change like '%MCO-%' then LTRIM(RTRIM(SUBSTRING(change,10,19) ))
when change like '%ECO-%' then LTRIM(RTRIM(SUBSTRING(change,10,19) ))
else '' end x from table
You can parse out the values from your requirements using SPLIT_STRING, outer apply, and a simple where clause without relying on hard coding any specific string length or position values, its dynamic.
SELECT D2.*
FROM
(
select '18 MCO-005329'
union select 'A ECO-12239'
union select '0 ECO-25126'
union select 'X1 ECO-05963'
union select 'NA MCO-003778'
union select 'C ECO-08399'
union select 'MCO-003759'
union select 'ECO-00643217'
union select NULL
) T(Change)
outer apply
(
select value
from
string_split(Change, ' ') d
) d2
where d2.value like '%-%' or d2.value is null
If you dont want nulls then smiply remove or d2.value is null
https://learn.microsoft.com/en-us/sql/t-sql/queries/from-transact-sql?view=sql-server-ver15
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver15
You could use CHARINDEX() and RIGHT() as
SELECT *, RIGHT(Change, CHARINDEX('-', REVERSE(Change)) + 3)
FROM
(
VALUES
('18 MCO-005329'),
('A ECO-12239'),
('0 ECO-25126'),
('X1 ECO-05963'),
('NA MCO-003778'),
('C ECO-08399'),
('MCO-003759'),
('ECO-00643217'), ('hhh kkk-k'),
(NULL)
) T(Change)

Delimited Function in SQL to Split Data between semi-colon

I have the data below.
I'm only interested on program B. How do I change it into the table below using SQL syntax?
Below is my syntax but it doesn't give me what I want.
SELECT
SUBSTRING(Program, 0, CHARINDEX(';', Program)),
SUBSTRING(
SUBSTRING(Program, CHARINDEX(';', Program) + 1, LEN(Program)),
0,
CHARINDEX(';', SUBSTRING(Program, CHARINDEX(';', Program) + 1,
LEN(Program)))),
REVERSE(SUBSTRING(REVERSE(Program), 0, CHARINDEX(';', REVERSE(Program)))),
File_Count
FROM DataBase1
WHERE Program LIKE '%B%'
Thanks guys for your help.
Adhi
Try this:
SELECT
CASE WHEN PATINDEX('%B[0-9][0-9]%', Program)>0 THEN SUBSTRING(Program, PATINDEX('%B[0-9][0-9]%', Program) - 1, 4)
WHEN PATINDEX('%B[0-9]%', Program)>0 THEN SUBSTRING(Program, PATINDEX('%B[0-9]%', Program) - 1, 3)
ELSE '' END
FROM DataBase1
First WHEN is responsible for extracting pattern B[0-9][0-9], i.e. when B is followed by two digits, second one is for extracting B followed by one digits. Default is returning empty string, when no match is found. If you are interested in extracting pattern B followed by three digits, you need to add another when (as the first case), enter pattern B[-9][0-9][0-9] instead of B[0-9][0-9] and change last number from 4 to 5 (length of string that is extracted).
PATINDEX returns position where the match is found.
If you use PostgreSql you can try next solution.
First create temp table with data:
CREATE TABLE temp.test AS (
SELECT 'A1, B1' AS program, 1 AS file_count
UNION
SELECT 'B2', 1
UNION
SELECT 'A2, B3', 1
UNION
SELECT 'B4', 1
UNION
SELECT 'A3, B5', 2
UNION
SELECT 'B6', 2
UNION
SELECT 'B7', 2
UNION
SELECT 'B8', 1
UNION
SELECT 'B9', 1
UNION
SELECT 'C1;D1;A4;B10', 1
UNION
SELECT 'C2;D2;B11', 1
UNION
SELECT 'C3,D3,A5,B12', 1
UNION
SELECT 'C4;B14;D4;B11,B13', 1
);
I suggested that in one program cell can contains several B values (last select).
After that use regexp_matches to find all B in cell and select for each file_count value(first inner select) and after that sum by each of program:
SELECT
b_program,
sum(file_count)
FROM (
SELECT
(SELECT regexp_matches(program, 'B\d+')) [1] AS b_program,
file_count
FROM temp.test
WHERE upper(program) LIKE '%B%') bpt
GROUP BY b_program
ORDER BY b_program;

Query Split string into rows

I have a table that looks like this:
ID Value
1 1,10
2 7,9
I want my result to look like this:
ID Value
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
2 7
2 8
2 9
I'm after both a range between 2 numbers with , as the delimiter (there can only be one delimiter in the value) and how to split this into rows.
Splitting the comma separated numbers is a small part of this problem. The parsing should be done in the application and the range stored in separate columns. For more than one reason: Storing numbers as strings is a bad idea. Storing two attributes in a single column is a bad idea. And, actually, storing unsanitized user input in the database is also often a bad idea.
In any case, one way to generate the list of numbers is to use a recursive CTE:
with t as (
select t.*, cast(left(value, charindex(',', value) - 1) as int) as first,
cast(substring(value, charindex(',', value) + 1, 100) as int) as last
from table t
),
cte as (
select t.id, t.first as value, t.last
from t
union all
select cte.id, cte.value + 1, cte.last
from cte
where cte.value < cte.last
)
select id, value
from cte
order by id, value;
You may need to fiddle with the value of MAXRECURSION if the ranges are really big.
Any table that a field with multiple values such as this is a problem in terms of design. The only way to deal with these records as it is is to split the values on the delimiter and put them into a temporary table, implement custom splitting code, integrate a CTE as noted, or redesign the original table to put the comma-delimited fields into separate fields, eg
ID LOWLIMIT HILIMIT
1 1 10
similar with Gordon Linoff variant, but has some difference
--create temp table for data sample
DECLARE #Yourdata AS TABLE ( id INT, VALUE VARCHAR(20) )
INSERT #Yourdata
( id, VALUE )
VALUES ( 1, '1,10' ),
( 2, '7,9' )
--final query
;WITH Tally
AS ( SELECT MIN(CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))) AS MinV ,
MAX(CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))) AS MaxV
FROM #yourdata AS y
UNION ALL
SELECT MinV = MinV + 1 , MaxV
FROM Tally
WHERE MinV < Maxv
)
SELECT y.id , t.minV AS value
FROM #yourdata AS y
JOIN tally AS t ON t.MinV BETWEEN CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))
AND CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))
ORDER BY id, minV
OPTION ( MAXRECURSION 999 ) --change it if required
output

Check palindrome without using string functions with condition

I have a table EmployeeTable.
If I want only that records where employeename have character of 1 to 5
will be palindrome and there also condition like total character is more then 10 then 4 to 8 if character less then 7 then 2 to 5 and if character less then 5 then all char will be checked and there that are palindrome then only display.
Examples :- neen will be display
neetan not selected
kiratitamara will be selected
I try this something on string function like FOR first case like name less then 5 character long
SELECT SUBSTRING(EmployeeName,1,5),* from EmaployeeTable where
REVERSE (SUBSTRING(EmployeeName,1,5))=SUBSTRING(EmployeeName,1,5)
I want to do that without string functions,
Can anyone help me on this?
You need at least SUBSTRING(), I have a solution like this:
(In SQL Server)
DECLARE #txt varchar(max) = 'abcba'
;WITH CTE (cNo, cChar) AS (
SELECT 1, SUBSTRING(#txt, 1, 1)
UNION ALL
SELECT cNo + 1, SUBSTRING(#txt, cNo + 1, 1)
FROM CTE
WHERE SUBSTRING(#txt, cNo + 1, 1) <> ''
)
SELECT COUNT(*)
FROM (
SELECT *, ROW_NUMBER() OVER (ORDER BY cNo DESC) as cRevNo
FROM CTE t1 CROSS JOIN
(SELECT Max(cNo) AS strLength FROM CTE) t2) dt
WHERE
dt.cNo <= dt.strLength / 2
AND
dt.cChar <> (SELECT dti.cChar FROM CTE dti WHERE dti.cNo = cRevNo)
The result will shows the count of differences and 0 means no differences.
Note :
Current solution is Non-Case-Sensitive for change it to a Case-Sensitive you need to check the strings in a case-sensitive collation like Latin1_General_BIN
You can use this solution as a SVF or something like that.
I dont realy understand why you dont want to use string functions in your query, but here is one solution. Compute everything beforehand:
Add Column:
ALTER TABLE EmployeeTable
ADD SubString AS
SUBSTRING(EmployeeName,
(
CASE WHEN LEN(EmployeeName)>10
THEN 4
WHEN LEN(EmployeeName)>7
THEN 2
ELSE 1 END
)
,
(
CASE WHEN LEN(EmployeeName)>10
THEN 8
WHEN LEN(EmployeeName)>7
THEN 5
ELSE 5 END
)
PERSISTED
GO
ALTER TABLE EmployeeTable
ADD Palindrome AS
REVERSE(SUBSTRING(EmployeeName,
(
CASE WHEN LEN(EmployeeName)>10
THEN 4
WHEN LEN(EmployeeName)>7
THEN 2
ELSE 1 END
)
,
(
CASE WHEN LEN(EmployeeName)>10
THEN 8
WHEN LEN(EmployeeName)>7
THEN 5
ELSE 5 END
)) PERSISTED
GO
Then your query will looks like:
SELECT * from EmaployeeTable
where Palindrome = SubString
BUT!
This is not a good idea. Please tell us, why you dont want to use string functios.
You could do it building a list of palindrome words using a recursive query that generates palindrome words till a length o n characters and then selects employees with the name matching a palindrome word. This may be a really inefficient way, but it does the trick
This is a sample query for Oracle, PostgreSQL should support this feature as well with little differences on syntax. I don't know about other RDBMS.
with EmployeeTable AS (
SELECT 'ADA' AS employeename
FROM DUAL
UNION ALL
SELECT 'IDA' AS employeename
FROM DUAL
UNION ALL
SELECT 'JACK' AS employeename
FROM DUAL
), letters as (
select chr(ascii('A') + rownum - 1) as letter
from dual
connect by ascii('A') + rownum - 1 <= ascii('Z')
), palindromes(word, len ) as (
SELECT WORD, LEN
FROM (
select CAST(NULL AS VARCHAR2(100)) as word, 0 as len
from DUAL
union all
select letter as word, 1 as len
from letters
)
union all
select l.letter||p.word||l.letter AS WORD, len + 1 AS LEN
from palindromes p
cross join letters l
where len <= 4
)
SEARCH BREADTH FIRST BY word SET order1
CYCLE word SET is_cycle TO 'Y' DEFAULT 'N'
select *
from EmployeeTable
WHERE employeename IN (
SELECT WORD
FROM palindromes
)
DECLARE #cPalindrome VARCHAR(100) = 'SUBI NO ONIBUS'
SET #cPalindrome = REPLACE(#cPalindrome, ' ', '')
;WITH tPalindromo (iNo) AS (
SELECT 1
WHERE SUBSTRING(#cPalindrome, 1, 1) = SUBSTRING(#cPalindrome, LEN(#cPalindrome), 1)
UNION ALL
SELECT iNo + 1
FROM tPalindromo
WHERE SUBSTRING(#cPalindrome, iNo + 1, 1) = SUBSTRING(#cPalindrome, LEN(#cPalindrome) - iNo, 1)
AND LEN(#cPalindrome) > iNo
)
SELECT IIF(MAX(iNo) = LEN(#cPalindrome), 'PALINDROME', 'NOT PALINDROME')
FROM tPalindromo

is there a PRODUCT function like there is a SUM function in Oracle SQL?

I have a coworker looking for this, and I don't recall ever running into anything like that.
Is there a reasonable technique that would let you simulate it?
SELECT PRODUCT(X)
FROM
(
SELECT 3 X FROM DUAL
UNION ALL
SELECT 5 X FROM DUAL
UNION ALL
SELECT 2 X FROM DUAL
)
would yield 30
select exp(sum(ln(col)))
from table;
edit:
if col always > 0
DECLARE #a int
SET #a = 1
-- re-assign #a for each row in the result
-- as what #a was before * the value in the row
SELECT #a = #a * amount
FROM theTable
There's a way to do string concat that is similiar:
DECLARE #b varchar(max)
SET #b = ""
SELECT #b = #b + CustomerName
FROM Customers
Here's another way to do it. This is definitely the longer way to do it but it was part of a fun project.
You've got to reach back to school for this one, lol. They key to remember here is that LOG is the inverse of Exponent.
LOG10(X*Y) = LOG10(X) + LOG10(Y)
or
ln(X*Y) = ln(X) + ln(Y) (ln = natural log, or simply Log base 10)
Example
If X=5 and Y=6
X * Y = 30
ln(5) + ln(6) = 3.4
ln(30) = 3.4
e^3.4 = 30, so does 5 x 6
EXP(3.4) = 30
So above, if 5 and 6 each occupied a row in the table, we take the natural log of each value, sum up the rows, then take the exponent of the sum to get 30.
Below is the code in a SQL statement for SQL Server. Some editing is likely required to make it run on Oracle. Hopefully it's not a big difference but I suspect at least the CASE statement isn't the same on Oracle. You'll notice some extra stuff in there to test if the sign of the row is negative.
CREATE TABLE DUAL (VAL INT NOT NULL)
INSERT DUAL VALUES (3)
INSERT DUAL VALUES (5)
INSERT DUAL VALUES (2)
SELECT
CASE SUM(CASE WHEN SIGN(VAL) = -1 THEN 1 ELSE 0 END) % 2
WHEN 1 THEN -1
ELSE 1
END
* CASE
WHEN SUM(VAL) = 0 THEN 0
WHEN SUM(VAL) IS NOT NULL THEN EXP(SUM(LOG(ABS(CASE WHEN SIGN(VAL) <> 0 THEN VAL END))))
ELSE NULL
END
* CASE MIN(ABS(VAL)) WHEN 0 THEN 0 ELSE 1 END
AS PRODUCT
FROM DUAL
The accepted answer by tuinstoel is correct, of course:
select exp(sum(ln(col)))
from table;
But notice that if col is of type NUMBER, you will find tremendous performance improvement when using BINARY_DOUBLE instead. Ideally, you would have a BINARY_DOUBLE column in your table, but if that's not possible, you can still cast col to BINARY_DOUBLE. I got a 100x improvement in a simple test that I documented here, for this cast:
select exp(sum(ln(cast(col as binary_double))))
from table;
Is there a reasonable technique that would let you simulate it?
One technique could be using LISTAGG to generate product_expression string and XMLTABLE + GETXMLTYPE to evaluate it:
WITH cte AS (
SELECT grp, LISTAGG(l, '*') AS product_expression
FROM t
GROUP BY grp
)
SELECT c.*, s.val AS product_value
FROM cte c
CROSS APPLY(
SELECT *
FROM XMLTABLE('/ROWSET/ROW/*'
PASSING dbms_xmlgen.getXMLType('SELECT ' || c.product_expression || ' FROM dual')
COLUMNS val NUMBER PATH '.')
) s;
db<>fiddle demo
Output:
+------+---------------------+---------------+
| GRP | PRODUCT_EXPRESSION | PRODUCT_VALUE |
+------+---------------------+---------------+
| b | 2*6 | 12 |
| a | 3*5*7 | 105 |
+------+---------------------+---------------+
More roboust version with handling single NULL value in the group:
WITH cte AS (
SELECT grp, LISTAGG(l, '*') AS product_expression
FROM t
GROUP BY grp
)
SELECT c.*, s.val AS product_value
FROM cte c
OUTER APPLY(
SELECT *
FROM XMLTABLE('/ROWSET/ROW/*'
passing dbms_xmlgen.getXMLType('SELECT ' || c.product_expression || ' FROM dual')
COLUMNS val NUMBER PATH '.')
WHERE c.product_expression IS NOT NULL
) s;
db<>fiddle demo
*CROSS/OUTER APPLY(Oracle 12c) is used for convenience and could be replaced with nested subqueries.
This approach could be used for generating different aggregation functions.
There are many different implmentations of "SQL". When you say "does sql have" are you referring to a specific ANSI version of SQL, or a vendor specific implementation. DavidB's answer is one that works in a few different environments I have tested but depending on your environment you could write or find a function exactly like what you are asking for. Say you were using Microsoft SQL Server 2005, then a possible solution would be to write a custom aggregator in .net code named PRODUCT which would allow your original query to work exactly as you have written it.
In c# you might have to do:
SELECT EXP(SUM(LOG([col])))
FROM table;