How to populate column with list from another table? - sql

Suppose I have three tables: STORES, STATES, and STORES_STATES.
STORES contains records of individual stores
STATES just contains a list of all 50 US states
STORES_STATES is a join table that contains pairs of stores and states to show which stores are available in which states
Consider the following tables (can't figure out how to format an ASCII table here):
What I need is a SELECT statement that will return each store in one column, and a list of states in another:
How do I combine the results of a subquery into a single column like this?
I would imagine it would be similar to this query:
SELECT
STORE_NAME,
(SELECT STATE_ABV FROM STORES_STATES WHERE STORES_STATES.STORE_ID = STORES.ID)
FROM STORES;
But this obviously fails since the subquery returns more than one result.

You can use APPLY :
SELECT s.store_name, STUFF(ss.state_abv, 1, 1, '') AS States
FROM stores s CROSS APPLY
( SELECT ', '+ss.state_abv
FROM stores_state ss
WHERE ss.store_id = s.id
FOR XML PATH('')
) ss(state_abv);

your two options are either the STRING_AGG function in the newest versions of SQL Server, or using an XML concatenation technique as described in this answer:
How to concatenate text from multiple rows into a single text string in SQL server?
The XML method is messy-looking in your code and difficult to remember - I always have to look up the syntax - but it's actually quite fast.

You can use STUFF() function along side with FOR XML PATH('') and join both tables on StoreID
CREATE TABLE Stores
(
ID INT,
StoreName VARCHAR(45)
);
CREATE TABLE States
(
StoreID INT,
StateABV VARCHAR(45)
);
INSERT INTO Stores VALUES
(1, 'Wolmart'), (2, 'Costco'), (3, 'Croegers');
INSERT INTO States VALUES
(1, 'NY'),
(1, 'WY'),
(1, 'MI'),
(2, 'AL'),
(2, 'GA'),
(2, 'TX'),
(3, 'FL');
SELECT SR.StoreName,
STUFF(
(
SELECT ',' + ST.StateABV
FROM States ST
WHERE ST.StoreID = SR.ID
FOR XML PATH('')
), 1, 1, ''
) State
FROM Stores SR;
Returns:
+-----------+----------+
| StoreName | State |
+-----------+----------+
| Wolmart | NY,WY,MI |
| Costco | AL,GA,TX |
| Croegers | FL |
+-----------+----------+

Related

Get Ids from constant list for which there are no rows in corresponding table

Let say I have a table Vehicles(Id, Name) with below values:
1 Car
2 Bike
3 Bus
and a constant list of Ids:
1, 2, 3, 4, 5
I want to write a query returning Ids from above list for which there are no rows in Vehicles table. In the above example it should return:
4, 5
But when I add new row to Vehicles table:
4 Plane
It should return only:
5
And similarly, when from the first version of Vehicle table I remove the third row (3, Bus) my query should return:
3, 4, 5
I tried with exist operator but it doesn't provide me correct results:
select top v.Id from Vehicle v where Not Exists ( select v2.Id from Vehicle v2 where v.id = v2.id and v2.id in ( 1, 2, 3, 4, 5 ))
You need to treat your "list" as a dataset, and then use the EXISTS:
SELECT V.I
FROM (VALUES(1),(2),(3),(4),(5))V(I) --Presumably this would be a table (type parameter),
--or a delimited string split into rows
WHERE NOT EXISTS (SELECT 1
FROM dbo.YourTable YT
WHERE YT.YourColumn = V.I);
Please try the following solution.
It is using EXCEPT set operator.
Set Operators - EXCEPT and INTERSECT (Transact-SQL)
SQL
-- DDL and sample data population, start
DECLARE #Vehicles TABLE (ID INT PRIMARY KEY, vehicleType VARCHAR(30));
INSERT INTO #Vehicles (ID, vehicleType) VALUES
(1, 'Car'),
(2, 'Bike'),
(3, 'Bus');
-- DDL and sample data population, end
DECLARE #vehicleList VARCHAR(20) = '1, 2, 3, 4, 5'
, #separator CHAR(1) = ',';
SELECT TRIM(value) AS missingID
FROM STRING_SPLIT(#vehicleList, #separator)
EXCEPT
SELECT ID FROM #Vehicles;
Output
+-----------+
| missingID |
+-----------+
| 4 |
| 5 |
+-----------+
In SQL we store our values in tables. We therefore store your list in a table.
It is then simple to work with it and we can easily find the information wanted.
I fully agree that it is possible to use other functions to solve the problem. It is more intelligent to implement database design to use basic SQL. It will run faster, be easier to maintain and will scale for a table of a million rows without any problems. When we add the 4th mode of transport we don't have to modify anything else.
CREATE TABLE vehicules(
id int, name varchar(25));
INSERT INTO vehicules VALUES
(1 ,'Car'),
(2 ,'Bike'),
(3 ,'Bus');
CREATE TABLE ids (iid int)
INSERT INTO ids VALUES
(1),(2),(3),(4),(5);
CREATE VIEW unknownIds AS
SELECT iid unknown_id FROM ids
LEFT JOIN vehicules
ON iid = id
WHERE id IS NULL;
SELECT * FROM unknownIds;
| unknown_id |
| ---------: |
| 4 |
| 5 |
INSERT INTO vehicules VALUES (4,'Plane')
SELECT * FROM unknownIds;
| unknown_id |
| ---------: |
| 5 |
db<>fiddle here

MS SQL: Transform delimeted string with key value pairs into table where keys are column names

I have a MS SQL table that has a message field containing a string with key value pairs in comma delimited format. Example:
id
date
message
1
11-5-2021
species=cat,color=black,says=meow
I need to read the data from tables message field and insert it into a table where keys are column names.
Format of the strings:
species=cat,color=black,says=meow
And this should be transformed into table as follows:
species
color
says
cat
black
meow
The order of key value pairs is not fixed in the message. Message can also contain additional keys that should be ignored.
How can I achieve this using MS SQL?
It is so much easier to implement by using JSON.
It will work starting from SQL Server 2016 onwards.
This way all the scenarios are taken in the account. I added them to the DDL and sample data population section in the T-SQL.
The order of key value pairs is not fixed in the message. Message can
also contain additional keys that should be ignored.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, [Date] DATE, Message VARCHAR(500));
INSERT INTO #tbl VALUES
('2021-05-01', 'species=cat,color=black,says=meow'),
('2021-05-11', 'species=dog,says=bark,comment=wow,color=white');
-- DDL and sample data population, end
WITH rs AS
(
SELECT *
, '[{"' + REPLACE(REPLACE(Message
, '=', '":"')
, ',', '","') + '"}]' AS jsondata
FROM #tbl
)
SELECT rs.ID, rs.Date, report.*
FROM rs
CROSS APPLY OPENJSON(jsondata)
WITH
(
[species] VARCHAR(10) '$.species'
, [color] VARCHAR(10) '$.color'
, [says] VARCHAR(30) '$.says'
) AS report;
Output
+----+------------+---------+-------+------+
| ID | Date | species | color | says |
+----+------------+---------+-------+------+
| 1 | 2021-05-01 | cat | black | meow |
| 2 | 2021-05-11 | dog | white | bark |
+----+------------+---------+-------+------+
You can use string_split() and some string operations:
select t.*, ss.*
from t cross apply
(select max(case when s.value like 'color=%'
then stuff(s.value, 1, 6, '')
end) as color,
max(case when s.value like 'says=%'
then stuff(s.value, 1, 5, '')
end) as s
from string_split(t.message, ',') s
) ss
Assuming you are using a fully supported version of SQL Server you could do something like this:
SELECT MAX(CASE PN.ColumnName WHEN 'species' THEN PN.ColumnValue END) AS Species,
MAX(CASE PN.ColumnName WHEN 'color' THEN PN.ColumnValue END) AS Color,
MAX(CASE PN.ColumnName WHEN 'says' THEN PN.ColumnValue END) AS Says
FROM (VALUES(1,CONVERT(date,'20210511'),'species=cat,color=black,says=meow'))V(id,date,message)
CROSS APPLY STRING_SPLIT(V.message,',') SS
CROSS APPLY (VALUES(PARSENAME(REPLACE(SS.[value],'=','.'),2),PARSENAME(REPLACE(SS.[value],'=','.'),1)))PN(ColumnName, ColumnValue);
Hopefully the reason you are doing this exercise is the normalise your design. If you aren't, I suggest you do.

SQL get top level object from joins

Working on a query right now where we want to understand which business is referring the most downstream orders for us. I've put together a very basic table for demonstration purposes here with 4 businesses listed. Bar and Donut were both ultimately referred by Foo and I want to be able to show Foo as a business has generated X number of orders. Obviously getting the the single referral for Foo (from Bar) and Bar (from Donut) are simple joins. But how do you go from Bar to get back to Foo?
I'll add that I've done some more googling this AM and found a few very similar questions about the top level parent and most of the responses suggest recursive CTE. It's been awhile since I've dug deep into SQL stuff, but 8 years ago I know these were not overly popular. Is there another way around this? Perhaps better to just store that parent ID on the order table at the time of order?
+----+--------+--------------------+
| Id | Name | ReferralBusinessId |
+----+--------+--------------------+
| 1 | Foo | |
| 2 | Bar | 1 |
| 3 | Donut | 2 |
| 4 | Coffee | |
+----+--------+--------------------+
WITH RECURSIVE entity_hierarchy AS (
SELECT id, name, parent FROM entities WHERE name = 'Donut'
UNION
SELECT e.id, e.name, e.parent FROM entities e INNER JOIN entity_hierarchy eh on e.id = eh.parent
)
SELECT id, name, parent FROM entity_hierarchy;
SQL Fiddle Example
Assuming you're using SQL Server, you could use a query like the one below to generate a hierarchical Id path for a particular business.
declare #tbl as table (Id int, Name varchar(30), ReferralBusinessId int)
insert into #tbl (id, Name, ReferralBusinessId) values
(1, 'Foo', null),
(2, 'Bar', 1),
(3, 'Donut', 2),
(4, 'Coffee', null);
;WITH business AS (
SELECT Id, Name, ReferralBusinessId
, 0 AS Level
, CAST(Id AS VARCHAR(255)) AS Path
FROM #tbl
UNION ALL
SELECT R.Id, R.Name, R.ReferralBusinessId
, Level + 1
, CAST(Path + '.' + CAST(R.Id AS VARCHAR(255)) AS VARCHAR(255))
FROM #tbl R
INNER JOIN business b ON b.Id = R.ReferralBusinessId
)
SELECT * FROM business ORDER BY Path

SQL select query order by on where in

How Can I make the order by based on what I input on where?
example query
select * from student where stud_id in (
'5',
'3',
'4'
)
the result would be
id| name |
5 | John |
3 | Erik |
4 | Michael |
Kindly help me thanks.
One method is with a derived table:
select s.*
from student s cross join
(values (5, 1), (3, 2), (4, 3)
) v(stud_id, ord)
on v.stud_id = s.stud_in
order by v.ord;
stud_id looks like a number so I dropped the single quotes. Numbers should be compared to numbers. If it is really a string, then use the single quotes.
As Gordon mentioned, you need something to provide order. An IN clause doesn't have a pre-defined order, just like a table doesn't. Rather than numbering the row order yourself, you could have a table variable do it like this:
DECLARE TABLE #StudentIDs
(
StudentIDKey int IDENTITY(1,1) PRIMARY KEY,
StudentID int
);
INSERT #StudentIDs (StudentID)
VALUES
(5),
(3),
(4);
SELECT *
FROM Student AS s
INNER JOIN #StudentIDs AS id
ON s.StudentID = id.StudentID
ORDER BY id.StudentIDKey;
That should be far easier if you have a lot of values to work with.
Hope that helps.

Select query to get all data from junction table to one field

I have 2 tables and 1 junction table:
table 1 (Log): | Id | Title | Date | ...
table 2 (Category): | Id | Title | ...
junction table between table 1 and 2:
LogCategory: | Id | LogId | CategoryId
now, I want a sql query to get all logs with all categories title in one field,
something like this:
LogId, LogTitle, ..., Categories(that contains all category title assigned to this log id)
can any one help me solve this? thanks
Try this code:
DECLARE #results TABLE
(
idLog int,
LogTitle varchar(20),
idCategory int,
CategoryTitle varchar(20)
)
INSERT INTO #results
SELECT l.idLog, l.LogTitle, c.idCategory, c.CategoryTitle
FROM
LogCategory lc
INNER JOIN Log l
ON lc.IdLog = l.IdLog
INNER JOIN Category c
ON lc.IdCategory = c.IdCategory
SELECT DISTINCT
idLog,
LogTitle,
STUFF (
(SELECT ', ' + r1.CategoryTitle
FROM #results r1
WHERE r1.idLog = r2.idLog
ORDER BY r1.idLog
FOR XML PATH ('')
), 1, 2, '')
FROM
#results r2
Here you have a simple SQL Fiddle example
I'm sure this query can be written using only one select, but this way it is readable and I can explain what the code does.
The first select takes all Log - Category matches into a table variable.
The second part uses FOR XML to select the category names and return the result in an XML instead of in a table. by using FOR XML PATH ('') and placing a ', ' in the select, all the XML tags are removed from the result.
And finally, the STUFF instruction replaces the initial ', ' characters of every row and writes an empty string instead, this way the string formatting is correct.