Finding a dynamic intersection of a table - sql

I have a query that will return some results like
London 111
London 222
London 333
Manchester 333
Liverpool 111
Liverpool 222
Liverpool 333
Leeds 111
Leeds 222
Leeds 333
My stored procedure takes in a user defined table that is a list of codes like
111 and 222, but it could take in any number of codes depending on user input from a website
Given that 111 and 333 are passed in I want the SP to return London, Liverpool and Leeds (not manchester) because
They all have records with both these codes
Any clues on how I can achieve this?
Thanks!

Assuming you have two tables like this:
CREATE table towns (town varchar(16), code int);
insert into towns values
('London', 111),('London', 222),
('London', 333),('Manchester', 333),
('Liverpool', 111),('Liverpool', 222),
('Liverpool', 333),('Leeds', 111),
('Leeds' ,222),('Leeds',333);
create table codes (ccde int);
insert into codes values (111),(333);
You should try
SELECT town, code FROM towns INNER JOIN codes on ccde=code GROUP BY town
HAVING COUNT(*)=(select count(*) from codes)
Please check out this edited version. My first try was obviously wrong.
You can find a demo here: http://rextester.com/DCWD90908
Edit
Just noticed that my command would give wrong results if there isn't a unique restriction on the columns town, code in towns. In case there can be double records in towns, like 'Manchester', 333 appearing twice, then the following will still deliver the right results:
SELECT town FROM
(SELECT DISTINCT * FROM towns) t
INNER JOIN codes ON ccde=code GROUP BY town
HAVING COUNT(*)=(SELECT DISTINCT COUNT(*) FROM codes)
http://rextester.com/RZJK41394

Throw the query results into a temporary table and re-query it like this.
You can do a basic SELECT query.
SELECT name , code
FROM [table] AS t1
INNER JOIN [table] AS t2
ON t1.name = t2.name
WHERE t1.code = '111' AND t2.code = '333'
This will get inefficient for larger data sets though.
Another method would be to use ROW_NUMBER() and a sub-SELECT.
SELECT DISTINCT name FROM (
SELECT name , ROW_NUMBER () OVER (PARTITION BY name ORDER BY code) AS row_count
FROM [table] AS t1
WHERE t1.code IN ('333' , '111')
GROUP BY t1.name, t1.code ) AS list
WHERE list.row_count > 1

Related

LIMIT by distinct values in PostgreSQL

I have a table of contacts with phone numbers similar to this:
Name Phone
Alice 11
Alice 33
Bob 22
Bob 44
Charlie 12
Charlie 55
I can't figure out how to query such a table with LIMITing the rows not just by plain count but by distinct names. For example, if I had a magic LIMIT_BY clause, it would work like this:
SELECT * FROM "Contacts" ORDER BY "Phone" LIMIT_BY("Name") 1
Alice 11
Alice 33
-- ^ only the first contact
SELECT * FROM "Contacts" ORDER BY "Phone" LIMIT_BY("Name") 2
Alice 11
Charlie 12
Alice 33
Charlie 55
-- ^ now with Charlie because his phone 12 goes right after 11. Bob isn't here because he's third, beyond the limit
How could I achieve this result?
In other words, select all rows containing top N distinct Names ordered by Phone
I don't think that PostgreSQL provides any particularly efficient way to do this, but for 6 rows it doesn't need to be very efficient. You could do a subquery to compute which people you want to see, then join that subquery back against the full table.
select * from
"Contacts" join
(select name from "Contacts" group by name order by min(phone) limit 2) as limited
using (name)
You could put the subquery in an IN-list rather than a JOIN, but that often performs worse.
If you want all names that are in the first n rows, you can use in:
select t.*
from t
where t.name in (select t2.name
from t t2
order by t2.phone
limit 2
);
If you want the first n names by phone:
select t.*
from t
where t.name in (select t2.name
from t t2
group by t2.name
order by min(t2.phone)
limit 2
);
try this:
SELECT distinct X.name
,X.phone
FROM (
SELECT *
FROM (
SELECT name
,rn
FROM (
SELECT name
,phone
,row_number() OVER (
ORDER BY phone
) rn
FROM "Contacts"
) AA
) DD
WHERE rn <= 2 --rn is the "limit" variable
) EE
,"Contacts" X
WHERE EE.name = X.name
above seems to be working correctly on following dataset:
create table "Contacts" (name text, phone text);
insert into "Contacts" (name, phone) VALUES
('Alice', '11'),
('Alice', '33'),
('Bob', '22'),
('Bob', '44'),
('Charlie', '13'),
('Charlie', '55'),
('Dennis', '12'),
('Dennis', '66');

(Easy) SQL Not Returning Desired Column. Half-Working Query (Access)

I am trying to determine the change in city population grouped by state then city between two tables which look like this:
**TABLE 1** **TABLE 2**
NY New York 10,000 NY New York -3000
NY Syracuse 5,000 NY Syracuse 5000
PA Phila 12,000 PA Phila 1000
PA Erie 11,000 PA Erie 4000
The query I'm using is the following:
SELECT Table1.State, Table1.City, SUMtable1.populationsum-Nz(SUMtable2.populationsum,0) AS Total
FROM (SELECT SUM(table1.populationchange) AS popsum, table1.State
FROM Table1
GROUP BY table1.State, table1.City) AS SUMtable1
LEFT JOIN (SELECT SUM(table2.populationchange) AS rsum, table2.State
FROM table2
GROUP BY table2.State, table2.City) AS SUMtable2
ON SUMtable1.State= SUMtable2.State;
However, this only gives me this:
**RESULT**
NY 7000
NY 10000
PA 13000
PA 15000
I need it to also return the city. At this time, the query prompts me for a parameter value for the city. Leaving it blank and pressing enter gets me that result. How can I complete this query?
City is not part of FROM queries.
SELECT State,City, sum(possum) AS Total
FROM ((
SELECT SUM(table1.populationchange) AS popsum, table1.State as state,
Table1.City as city
FROM Table1
GROUP BY table1.State, table1.City)
Union All (
SELECT SUM(table2.populationchange) AS rsum, table2.State as state ,
Table2.City as city
FROM table2
GROUP BY table2.State, table2.City) )
Group by state,city
Same scenario with sample tables:
create table #temp1(c varchar(100),s varchar(100), p int);
create table #temp2(c varchar(100),s varchar(100), p int);
Insert into #temp1(c,s,p) values('ny','syc',10)
Insert into #temp1(c,s,p) values('ny','ny',10)
Insert into #temp1(c,s,p) values('Nc','ra',10)
Insert into #temp1(c,s,p) values('Ns','char',10)
Insert into #temp2(c,s,p) values('ny','syc',10)
Insert into #temp2(c,s,p) values('ny','ny',10)
Insert into #temp2(c,s,p) values('Nc','ra',10)
Insert into #temp2(c,s,p) values('Ns','char',10)
select result.s,result.c,sum(p) as total from (
select * from #temp1 union all select * from #temp2) as result group by result.c,result.s
Exact Query :
select res.state,res.city,sum(res.popultationchange) as totalchange
from(
select * from Table1 union all select * from Table2
) as res group by res.state,res.city
Hope this is what you need!

SQL Server: ORDER BY parameters in IN statement

I have a SQL statement that is the following:
SELECT A.ID, A.Name
FROM Properties A
WHERE A.ID IN (110, 105, 104, 106)
When I run this SQL statement, the output is ordered according to the IN list by ID automatically and returns
104 West
105 East
106 North
110 South
I want to know if it is possible to order by the order the parameters are listed within the IN clause. so it would return
110 South
105 East
104 West
106 North
I think the easiest way in SQL Server is to use a JOIN with VALUES:
SELECT p.ID, p.Name
FROM Properties p JOIN
(VALUES (110, 1), (105, 2), (104, 3), (106, 4)) ids(id, ordering)
ON p.id = a.id
ORDER BY ids.ordering;
Sure...
just add an Order clause with a case in it
SELECT A.ID, A.Name
FROM Properties A
WHERE A.ID IN (110,105,104,106)
Order By case A.ID
when 110 then 0
when 105 then 1
when 104 then 2
when 106 then 3 end
With the help of a parsing function which returns the sequence as well
SELECT B.Key_PS
, A.ID
, A.Name
FROM Properties A
Join (Select * from [dbo].[udf-Str-Parse]('110,105,104,106',',')) B on A.ID=B.Key_Value
WHERE A.ID IN (110,105,104,106)
Order by Key_PS
The UDF if you need
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimeter varchar(10))
--Usage: Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
-- Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
-- Select * from [dbo].[udf-Str-Parse]('id26,id46|id658,id967','|')
-- Select * from [dbo].[udf-Str-Parse]('hello world. It. is. . raining.today','.')
Returns #ReturnTable Table (Key_PS int IDENTITY(1,1), Key_Value varchar(max))
As
Begin
Declare #XML xml;Set #XML = Cast('<x>' + Replace(#String,#Delimeter,'</x><x>')+'</x>' as XML)
Insert Into #ReturnTable Select Key_Value = ltrim(rtrim(String.value('.', 'varchar(max)'))) FROM #XML.nodes('x') as T(String)
Return
End
The Parser alone would return
Select * from [dbo].[udf-Str-Parse]('110,105,104,106',',')
Key_PS Key_Value
1 110
2 105
3 104
4 106
What you could potentially do is:
Create a TVF that would split string and keep original order.
This questions seems to have this function already written: MS SQL: Select from a split string and retain the original order (keep in mind that there might be other approaches, not only those, covered in this question, I just gave it as an example to understand what function should do)
So now if you'd run this query:
SELECT *
FROM dbo.Split('110,105,104,106', ',') AS T;
It would bring back this table as a result.
items rownum
------------
110 1
105 2
104 3
106 4
Following that, you could simply query your table, join with this TVF passing your IDs as a parameter:
SELECT P.ID, P.Name
FROM Properties AS P
INNER JOIN dbo.Split('110,105,104,106', ',') AS T
ON T.items = P.ID
ORDER BY T.rownum;
This should retain order of parameters.
If you need better performance, I'd advice to put records from TVF into hash table, index it and then join with actual table. See query below:
SELECT T.items AS ID, T.rownum AS SortOrder
INTO #Temporary
FROM dbo.Split('110,105,104,106', ',') AS T;
CREATE CLUSTERED INDEX idx_Temporary_ID
ON #Temporary(ID);
SELECT P.ID, P.Name
FROM Properties AS P
INNER JOIN #Temporary AS T
ON T.ID = P.ID
ORDER BY T.SortOrder;
This should work better on larger data sets and equally well on small ones.
Here is a solution that does not rely on hard codes values or dynamic sql (to eliminate hard coding values).
I would build a table (maybe temp or variable) with OrderByValue and OrderBySort and insert from the application.
OrderByValue OrderBySort
110 1
105 2
104 3
106 4
Then I would join on the value and sort by the sort. The join will be the same as the In clause.
SELECT A.ID, A.Name
FROM Properties A
JOIN TempTable B On A.ID = B.OrderByValue
Order By B.OrderBySort
Another solution for this problem is prepare a temporary table for IN clause like
declare #InTable table (ID int, SortOrder int not null identity(1,1));
We can fill this temp table with your data in order you want
insert into #InTable values (110), (105), (104), (106);
At last we need to modify your question to use this temp table like this
select A.ID, A.Name
from Properties A
inner join #InTable as Sort on A.ID = Sort.ID
order by Sort.SortOrder
On the output you can see this
ID Name
110 South
105 East
104 West
106 North
In this solution you don't need to provide order in special way. You just need to insert values in order you want.

Query to exclude two or more records if they match a single value

I have a database table in which multiple customers can be assigned to multiple types. I am having trouble formulating a query that will exclude all customer records that match a certain type. For example:
ID CustomerName Type
=========================
111 John Smith TFS-A
111 John Smith PRO
111 John Smith RWAY
222 Jane Doe PRO
222 Jane Doe TFS-A
333 Richard Smalls PRO
444 Bob Rhoads PRO
555 Jacob Jones TFS-B
555 Jacob Jones TFS-A
What I want is to pull only those people who are marked PRO but not marked TFS. If they are PRO and TFS, exclude them.
Any help is greatly appreciated.
You can get all 'PRO' customers and use NOT EXISTS clause to exclude the ones that are also 'TFS':
SELECT DISTINCT ID, CustomerName
FROM mytable AS t1
WHERE [Type] = 'PRO' AND NOT EXISTS (SELECT 1
FROM mytable AS t2
WHERE t1.ID = t2.ID AND [Type] LIKE 'TFS%')
SQL Fiddle Demo
solution using EXCEPT
WITH TestData
AS (
SELECT *
FROM (
VALUES ( 111, 'John Smith', 'TFS-A' )
, ( 111, 'John Smith', 'PRO' )
, ( 111, 'John Smith', 'RWAY' )
, ( 222, 'Jane Doe', 'PRO' )
, ( 222, 'Jane Doe', 'TFS-A' )
, ( 333, 'Richard Smalls', 'PRO' )
, ( 444, 'Bob Rhoads', 'PRO' )
, ( 555, 'Jacob Jones', 'TFS-B' )
, ( 555, 'Jacob Jones', 'TFS-A' ))
AS t (ID, CustomerName, [Type])
)
SELECT ID, CustomerName
FROM TestData
WHERE [Type] = 'PRO'
EXCEPT
SELECT ID, CustomerName
FROM TestData
WHERE [Type] LIKE 'TFS%'
output result
Select DISTINCT(Customername),ID
FROM tablename
WHERE NOT (ID IN (SELECT ID FROM tablename WHERE type='PRO')
AND ID IN (SELECT ID FROM tablename WHERE type='TFS'))
EDIT: now added working TFS clause
Get all customers that do not have TYPE PRO AND TFS for example
SQLFIDDLE:http://sqlfiddle.com/#!9/da4f9/2
Try This :
SELECT *
FROM table a
WHERE Type = 'PRO'
AND NOT EXISTS(SELECT 1
FROM table b
WHERE a.ID = b.ID
AND LEFT(Type, 3) = 'TFS')
I know this question has been answered, but mine answer is different. Everyone else solutions involves two queries which means what I call "double-dipping". You have to look access the same table twice. It's better to avoid this when possible for better performance. Check this out:
SELECT ID,
CustomerName,
MIN([type]) AS [Type] --doesn't matter if it's MIN or MAX
FROM yourTable
WHERE [Type] = 'PRO' --only load values that matter. Ignore RWAY
OR [Type] LIKE 'TFS-_' --notice I use a "_" instead of "%". That because "_" is a wildcard for a single character
--instead of wildcard looking for any number of characters because normally it's best to be as narrow as possible to be more efficient
GROUP BY ID,CustomerName
HAVING SUM(CASE
WHEN [Type] = 'Pro' --This is where it returns values that only have type PRO
THEN 9999
ELSE 1
END
) = 9999
So let me explain my funky HAVING logic. So as you can see it's a SUM() so and for PRO it's 9999 and TFS-_ it's 1. So when the sum is EXACTLY 9999, then it's good. Why I can't just do a COUNT(*) = 1 is because if a value has only one TFS and no pro, it would be returned, which of course would be incorrect.
Results:
ID CustomerName Type
----------- -------------- -----
444 Bob Rhoads PRO
333 Richard Smalls PRO

One SQL statement for counting the records in the master table based on matching records in the detail table?

I have the following master table called Master and sample data
ID---------------Date
1 2014-09-07
2 2014-09-07
3 2014-09-08
The following details table called Details
masterId-------------Name
1 John Walsh
1 John Jones
2 John Carney
1 Peter Lewis
3 John Wilson
Now I want to find out the count of Master records (grouped on the Date column) whose corresponding details record with Name having the value "John".
I cannot figure how to write a single SQL statement for this job.
**Please note that join is needed in order to find master records for count. However, such join creates duplicate master records for count. I need to remove such duplicate records from being counted when grouping on the Date column in the Master table.
The correct results should be:
count: grouped on Date column
2 2014-09-07
1 2014-09-08
**
Thanks and regards!
This answer assumes the following
The Name field is always FirstName LastName
You are looking once and only once for the John firstname. The search criteria would be different, pending what you need
SELECT Date, Count(*)
FROM tblmaster
INNER JOIN tbldetails ON tblmaster.ID=tbldetails.masterId
WHERE NAME LIKE 'John%'
GROUP BY Date, tbldetails.masterId
What we're doing here is using a wilcard character in our string search to say "Look for John where any characters of any length follows".
Also, here is a way to create table variables based on what we're working with
DECLARE #tblmaster as table(
ID int,
[date] datetime
)
DECLARE #tbldetails as table(
masterID int,
name varchar(50)
)
INSERT INTO #tblmaster (ID,[date])
VALUES
(1,'2014-09-07'),(2,'2014-09-07'),(3,'2014-09-08')
INSERT INTO #tbldetails(masterID, name) VALUES
(1,'John Walsh'),
(1,'John Jones'),
(2,'John Carney'),
(1,'Peter Lewis'),
(3,'John Wilson')
Based on all comments below, this SQL statement in it's clunky glory should do the trick.
SELECT date,count(t1.ID) FROM #tblmaster mainTable INNER JOIN
(
SELECT ID, COUNT(*) as countOfAll
FROM #tblmaster t1
INNER JOIN #tbldetails t2 ON t1.ID=t2.masterId
WHERE NAME LIKE 'John%'
GROUP BY id)
as t1 on t1.ID = mainTable.id
GROUP BY mainTable.date
Is this what you want?
select date, count(distinct m.id)
from master m join
details d
on d.masterid = m.id
where name like '%John%'
group by date;