I have a code block in which I am looping over a record that contains two joined subqueries that contain equally named columns in different tables.
Now I seem to be able to access sq1 and sq2 in the record, but not the contents and I always get "could not identify column 'c1' in record data type", even if I add explicit aliases to the columns:
DO $$
DECLARE
r record;
BEGIN
FOR r IN SELECT sq1, sq2
FROM (SELECT t1.someColumn as c1, t2.someColumn as c2, ... FROM Table1 t1 JOIN Table2 t2 ...) sq1
JOIN (SELECT t1.someColumn as c1, t2.someColumn as c2, ... FROM Table1 t1 JOIN Table2 t2 ...) sq2
ON (sq1.joinColumn1 = sq2.joinColumn2 AND sq1.joinColumn2 = sq2.joinColumn1)
LOOP
INSERT INTO Table3 (column1, column2)
VALUES ((r.sq1).c1, (r.sq2).c1);
--^ error occurs here
END LOOP;
END$$;
I am looking for a way to access the records similar to the following way:
r.sq1.t1.someColumn
For access to the 3 level in your variable you must cast it as named record type. It might be table or type:
For type:
CREATE TYPE my_type AS (c1 int4, c2 int4, joinColumn1 int4);
For table:
CREATE TABLE my_type (c1 int4, c2 int4, joinColumn1 int4);
And after that you can do something like this:
DO $$
DECLARE
r record;
BEGIN
FOR r IN SELECT (sq1.*)::my_type AS sq1, (sq2.*)::my_type AS sq2
FROM (SELECT 10 as c1, 11 as c2, 1 as joinColumn1) sq1
JOIN (SELECT 20 as c1, 21 as c2, 1 as joinColumn2) sq2
ON (sq1.joinColumn1 = sq2.joinColumn2)
LOOP
RAISE NOTICE '%', r;
RAISE NOTICE '%', r.sq1;
RAISE NOTICE '% %', (r.sq1).c1, (r.sq2).c1;
INSERT INTO Table3 (column1, column2)
VALUES ((r.sq1).c1, (r.sq2).c1);
END LOOP;
END$$;
Given a string combination which is a calculation statement, how can I get the result, in this case is column cal in below code.
I know I can use case but is there any direct way to do the calculation?
create table tl_test
(
cl1 int
)
create table tl_test2
(
cl1 char(1)
)
insert into tl_test values (21), (43), (13), (36), (41)
insert into tl_test2 values ( '+'), ('-'), ('*'), ('/')
select *,
cast(c1 as varchar) + f1
+ cast(c2 as varchar) + f2
+ cast(c3 as varchar) + f3
+ cast(c4 as varchar) + f4
+ cast(c5 as varchar) as cal
from(
SELECT A.cl1 as c1, f1.cl1 as f1, b.cl1 as c2,f2.cl1 as f2, C.cl1 as c3, f3.cl1 as f3, D.cl1 as c4, f4.cl1 as f4, E.cl1 as c5
FROM TL_TEST A
CROSS JOIN TL_TEST2 f1
CROSS JOIN TL_TEST B
CROSS JOIN TL_TEST2 f2
CROSS JOIN TL_TEST C
CROSS JOIN TL_TEST2 f3
CROSS JOIN TL_TEST D
CROSS JOIN TL_TEST2 f4
CROSS JOIN TL_TEST E
)a
WHERE c1 != c2
and c1 != c3
and c1 != c4
and c1 != c5
and c2 != c3
and c2 != c4
and c2 != c5
and c3 != c4
and c3 != c5
and c4 != c5
You could store the result of the cal column in a string and use EXEC to calculate your answer.
DECLARE #sql NVARCHAR(MAX) = 'SELECT ' + (SELECT TOP 1 cal FROM result)
EXEC(#sql)
Read this related thread, especially the answer by Erland Sommarskog.
I'm sorry, there is no way to do this in pure ad-hoc T-SQL.
Possible workarounds:
1) You can use dynamic SQL, which has one flaw: Your example would be calculated for INTs
DECLARE #cmd VARCHAR(100)='SELECT (' + '21*41-36 / 13+43' /*Your formula coming from somewhere*/ + ')';
EXEC(#cmd);
Results 902
2) You can use XMLs implicit ability to calculate like this
SELECT CAST('' AS XML).value('21*41-36 div 13+43','float')
hint: "/" must be replaced with " div "
Results 901.23077
It's a pitty, that the XML type's .value() knows expressions and values. A value can be introduced from a table's column dynamically (sql:column("ColumnName")), but an expression must be a literal.
3) And you might include .Net-code as assembly (CLR function).
Conclusio
Sorry, there is no easy going...
If you need exact results you should use the XML approach in dynamically created SQL.
UPDATE Working example
DECLARE #tbl TABLE(ID INT IDENTITY,SomeFormula VARCHAR(100));
INSERT INTO #tbl(SomeFormula) VALUES
('1+2')
,('21*41-36/13+43')
,('(1+3)*4')
,('3.6 div (2.5-1)');
DECLARE #cmd VARCHAR(MAX)=
(
SELECT 'DECLARE #x XML=CAST('''' AS XML);' +
STUFF
(
(
SELECT ' UNION ALL SELECT ' + CAST(ID AS VARCHAR(MAX)) + ' AS ID, #x.value(''' + REPLACE(SomeFormula,'/',' div ') + ''',''float'')'
FROM #tbl
FOR XML PATH('')
),1,11,''
)
);
PRINT #cmd;
EXEC (#cmd);
This is the generated statement
DECLARE #x XML=CAST('' AS XML);
SELECT 1 AS ID, #x.value('1+2','float')
UNION ALL SELECT 2 AS ID, #x.value('21*41-36 div 13+43','float')
UNION ALL SELECT 3 AS ID, #x.value('(1+3)*4','float')
UNION ALL SELECT 4 AS ID, #x.value('3.6 div (2.5-1)','float')
And this is the result
ID
1 3
2 901,23077
3 16
4 2,4
table1
id col1 col2 col3...
table2
col_id col_name
3432 col1
5342 col2
6756 col3
Now I want to generate table 3 like this:
id col_name col_value col_id
Please note that col1, col2,col3... are not in order. Therefore I have to query table2 to obtain col_id ( I think pivot does not work here)
How can I do it in SQL?
It appears that you want a select like this:
SELECT t2.id,
CASE
WHEN t2.col_name='col1' THEN t1.col1
WHEN t2.col_name='col2' THEN t1.col2
WHEN t2.col_name='col2' THEN t1.col2
-- ... more columns
ELSE NULL
END
FROM table2 t2 LEFT JOIN table2 t1 ON t2.col_id = t1.id
You could also create a function, though this will be slower in practice:
CREATE OR REPLACE FUNCTION table1_col(id integer, name text) RETURNS text as $$
DECLARE
col_val text;
BEGIN
EXECUTE format('SELECT %s FROM table1 WHERE id=$1', name)
INTO col_val
USING id;
RETURN col_val;
END;
$$ LANGUAGE plpgsql;
SELECT table1_col(col_id,col_name) FROM table2;
Disclaimer: I'm dealing with a rather old legacy system so any comments telling me about poor design are redundant, although I do genuinely appreciate any such sentiment. There is a new version that solves most legacy problems but we still have to maintain the old system, so basically, we have to manage for now.
I have a table that looks like this (yes, that is a single column, I know):
And I need a view (for reporting purposes) that will dynamically process the data in said table and return this:
The values are \n-delimited (shudder) and you can assume there will always be the same number of values in each cell (9 in the example, although other databases could have 4 or 12 or any number), although I suppose having NULL-insertion in the event of missing values couldn't hurt. They will also always be in a matching order (as in the example, 'AUD', 'Australian Dollar', and '$' are all the first values in their respective cells, and so on).
I've found various approaches to splitting a single cell out into a view, but nothing that covers merging data in such a way as I require. Sitting at home with a cold has not helped my research capabilities. Help me StackOverflow, you're my only hope!
Bonus points for tidy, relatively readable SQL examples, although I'm anticipating messiness as a natural by-product of the hackish nature of my required solution.
Something like this. I didn't take the time to build out the tables, but it should be fairly obvious where you can replace my variables with your rows. You will also want to do a replace char(10) where I have used commas. You could package it up in a table valued function and then call as a view.
declare #xml1 xml
declare #xml2 xml
declare #xml3 xml
declare #c1 nvarchar(250)
declare #c2 nvarchar(250)
declare #c3 nvarchar(250)
set #c1 = N'AUD,CAD,EUR,GBP,JPY,NZD,USD,KES,CHF';
set #c2 = N'Australian Dollar,Canadian Dollar,Euro,Pound Sterling,Yen,New Zealand Dollar,United States Dollar,Kenyan Shilling, Swiss Franc';
set #c3 = N'$,$,C,L,Y,$,$,K,F';
-- you'd use replace(#c1, char(10), '</r><r>') etc etc for /n delimited code
set #xml1 = N'<root><r>' + replace(#c1,',','</r><r>') + '</r></root>';
set #xml2 = N'<root><r>' + replace(#c2,',','</r><r>') + '</r></root>';
set #xml3 = N'<root><r>' + replace(#c3,',','</r><r>') + '</r></root>';
select code.code, name.name, symbol.symbol
from
(select ROW_NUMBER() over (order by ##rowcount) as ck,
c.value('.','varchar(max)') as [code]
from #xml1.nodes('//root/r') as a(c)) as code
inner join
(select ROW_NUMBER() over (order by ##rowcount) as nk,
n.value('.','varchar(max)') as [name]
from #xml2.nodes('//root/r') as a(n)) as name on code.ck = name.nk
inner join
(select ROW_NUMBER() over (order by ##rowcount) as sk,
s.value('.','varchar(max)') as [symbol]
from #xml3.nodes('//root/r') as a(s)) as symbol on symbol.sk = name.nk
You can run this as a single script in SSMS for verification that it works. No schema necessary.
Using Jeff Moden's Tally Ho! CSV splitter:
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH
E1(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
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
and inline CTE data like this
with
data as (select Num,Currencies from (values
(1,'AUD'+char(10)+'CAD'+char(10)+'USD'+char(10)+'KES')
,(2,'Australian DOllar'+char(10)+'Canadian Dollar'+char(10)+'US Dollar'+char(10)+'Kenyan Shilling')
,(3,'$'+char(10)+'$'+char(10)+'$'+char(10)+'k')
)data(Num,Currencies)
),
The solution is as simple as this:
map as (select * from (values
(1,'Code')
,(2,'Name')
,(3,'Symbol')
)map(Num,Col )
)
select
ItemNumber
,max(Code) as Code
,max(Name) as Name
,max(Symbol) as Symbol
from (
select
map.Num
,map.Col
,c.Item
,c.ItemNumber
from data
join map
on map.Num = data.Num
cross apply dbo.DelimitedSplit8K(data.Currencies,char(10)) c
) t
pivot (max(Item) for Col in (Code,Name,Symbol)) pvt
group by ItemNumber
to give us:
ItemNumber Code Name Symbol
-------------- ---- -------------------- ---------------
1 AUD Australian DOllar $
2 CAD Canadian Dollar $
3 USD US Dollar $
4 KES Kenyan Shilling k
Hope this Helps. Run all together or replace the table variable with a temptable.
Sample Data:
IF OBJECT_ID(N'tempdb..#table') > 0
BEGIN
DROP TABLE #table
END
DECLARE #table TABLE(ATTRIBUTELVAUE VARCHAR(MAX))
INSERT INTO #table
SELECT
'AFN
ALL
DZD
USD
EUR
AOA
XCD
XCD
ARS'
INSERT INTO #table
SELECT
'Afghanistan
Albania
Algeria
American Samoa
Andorra
Angola
Anguilla
Antigua and Barbuda
Argentina'
INSERT INTO #table
SELECT
'AF
AL
DZ
AS
AD
AO
AI
AG
AR'
Query:
IF OBJECT_ID(N'tempdb..#TEMP') > 0
BEGIN
DROP TABLE #TEMP
END
DECLARE #StartLoop INT
DECLARE #EndLoop INT
DECLARE #Code TABLE (ID INT IDENTITY(1, 1),
Code VARCHAR(250))
DECLARE #Name TABLE (ID INT IDENTITY(1, 1),
Name VARCHAR(250))
DECLARE #Symbol TABLE (ID INT IDENTITY(1, 1),
Symbol VARCHAR(250))
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS ID,
*
INTO #Temp
FROM #table
SELECT #StartLoop = MIN(ID),
#EndLoop = MAX(ID)
FROM #Temp
WHILE #StartLoop <= #EndLoop
BEGIN
DECLARE #WorkingString VARCHAR(MAX)
SELECT #WorkingString = ATTRIBUTELVAUE + CHAR(10) + ' '
FROM #Temp
WHERE ID = #StartLoop
--print #WorkingString
WHILE CHARINDEX(CHAR(10), #WorkingString) > 0
BEGIN
DECLARE #SearchCharacter INT
DECLARE #WorkingStringLength INT
DECLARE #TempStringLength INT
DECLARE #TempString VARCHAR(MAX)
SET #WorkingStringLength = LEN(#WorkingString)
SET #SearchCharacter = CHARINDEX(CHAR(10), #WorkingString)
SET #TempString = SUBSTRING(#WorkingString, 1, #SearchCharacter - 1)
SET #TempStringLength = LEN(#TempString)
SET #WorkingString = SUBSTRING(#WorkingString, #SearchCharacter + 1, #WorkingStringLength)
SET #TempString = REPLACE(#TempString, CHAR(13), '')
IF #StartLoop = 1
BEGIN
INSERT INTO #Code
SELECT #TempString
END
IF #StartLoop = 2
BEGIN
INSERT INTO #Name
SELECT #TempString
END
IF #StartLoop = 3
BEGIN
INSERT INTO #Symbol
SELECT #TempString
END
END
SET #StartLoop = #StartLoop + 1
END
SELECT Code,
Name,
Symbol
FROM #Code AS c
JOIN #Name AS n
ON c.ID = n.ID
JOIN #Symbol AS s
ON s.ID = n.ID
Cleanup:
IF OBJECT_ID(N'tempdb..#TEMP') > 0
BEGIN
DROP TABLE #TEMP
END
IF OBJECT_ID(N'tempdb..#table') > 0
BEGIN
DROP TABLE #table
END
Because I needed a view, this ended up being my solution:
CREATE FUNCTION [dbo].[CurrencyTableGenerator]()
RETURNS
#CurrencyTable TABLE(
Code NVARCHAR(250)
,Name NVARCHAR(250)
,Symbol NVARCHAR(250)
)
AS
BEGIN
DECLARE #xml1 XML
DECLARE #xml2 XML
DECLARE #xml3 XML
DECLARE #C1 NVARCHAR(250)
DECLARE #C2 NVARCHAR(250)
DECLARE #c3 NVARCHAR(250)
SET #c1 = (SELECT ...)
SET #c2 = (SELECT ...)
SET #c3 = (SELECT ...)
SET #xml1 = N'<root><r>' + REPLACE(#c1, CHAR(10), '</r><r>') + '</r></root>';
SET #xml2 = N'<root><r>' + REPLACE(#c2, CHAR(10), '</r><r>') + '</r></root>';
SET #xml3 = N'<root><r>' + REPLACE(#c3, CHAR(10), '</r><r>') + '</r></root>';
INSERT INTO #CurrencyTable
SELECT Code.Code, Name.Name, Symbol.Symbol
FROM
(SELECT ROW_NUMBER() OVER (ORDER BY ##ROWCOUNT) AS ck,
c.value('.', 'VARCHAR(250)') AS [Code]
FROM #xml1.nodes('//root/r') AS a(c)) AS Code
INNER JOIN
(SELECT ROW_NUMBER() OVER (ORDER BY ##ROWCOUNT) AS nk,
n.value('.', 'VARCHAR(250)') AS [Name]
FROM #xml2.nodes('//root/r') AS a(n)) AS Name ON Code.ck = Name.nk
INNER JOIN
(SELECT ROW_NUMBER() OVER (ORDER BY ##ROWCOUNT) AS sk,
s.value('.', 'VARCHAR(250)') AS [Symbol]
FROM #xml3.nodes('//root/r') AS a(s)) AS Symbol ON Symbol.sk = Name.nk
RETURN
END
GO
CREATE VIEW [dbo].[CurrencyView]
AS
SELECT * FROM [dbo].[CurrencyTableGenerator]()
GO
Thanks to RThomas for the function.
Let me frame my question ....
I have say
Name
A
B
C
A
D
B
What I want is
ID Name
1 A
2 B
3 C
4 A
5 D
6 B
If I write
SELECT name, (SELECT COUNT(*) FROM #t AS i2 WHERE i2.Name <= i1.Name) As rn FROM #t AS i1
it will work fine if all the names are distinct/unique...What if they are not(as in this example)
Even NEWID() does not make the trick as it varies overtime?
I am using sql server 2000...
Please help
Here are 2 ways of solving it
1.
DECLARE #t TABLE ([ID] [int] IDENTITY(1,1), name CHAR)
INSERT #t VALUES ('b')
INSERT #t VALUES ('a')
INSERT #t VALUES ('c')
INSERT #t VALUES ('b')
SELECT * FROM #t
2.
DECLARE #t2 TABLE (name CHAR)
INSERT #t2 (name) VALUES ('b')
INSERT #t2 (name) VALUES ('a')
INSERT #t2 (name) VALUES ('c')
INSERT #t2 (name) VALUES ('b')
SELECT ID = ROW_NUMBER() OVER (ORDER BY b), name
FROM (SELECT name, null b FROM #t2) temp