Create Count On Table That References Itself - sql

I have a table that looks like below:
The table lists countries and regions (states, provinces, counties, etc) within those countries. I need to generate a count of all the regions within all countries. As you can see, each region has a ParentID which is the ID of the country in which you can find the region.
As an example, California is in USA, so its parent ID is 1 (which is the ID of USA).
So, the results from the simple table above should be:
USA: 2 and
Canada: 1
I have tried the following:
Select all values into a table which have ID a 1 (for USA)
Select all values into a table which have ID a 3 (for Canada)
Select all values into the USA table with Parent ID as 1
Select all values into the Canada table with Parent ID as 3
Do counts on both tables
The problem with the above approach is that if a new country is added, a count will not be automatically generated.
Any ideas on making this more dynamic?

You have to join the table with itself:
select t1.ID, t1.segment, count(distinct t2.ID)
from yourTable t1
join yourTable t2
on t1.ID = t2.parentID
where t1.parentID is null
group by t1.ID, t1.segment
The where clause ensures you that only "top level" rows will be displayed.

Perhaps it makes sense to re-format the data, incase there are other sorts of queries that you want to make in addition to a count of countries and regions.
CREATE TABLE #CountriesRegions
(
[ID] [int] NOT NULL,
parentid [int] NULL,
segment [nvarchar](50) NULL)
insert into #CountriesRegions values (1,null,'usa'), (2,1, 'california'), (3, null, 'canada'), (4, 3, 'quebec'), (5, 1, 'NY')
select * from #CountriesRegions
Create table #Country
([ID] [int] NOT NULL
,[country_name] [nvarchar](50) NOT NULL)
Insert into #Country select ID, segment AS country_name from #CountriesRegions where parentid IS NULL
select * from #Country
Create table #Region
([ID] [int] NOT NULL
,[country_id] [int] NOT NULL
,[region_name] [nvarchar](50) NOT NULL)
Insert into #Region select ID, parentid AS country_ID, segment AS region_name from #CountriesRegions where parentid IS NOT NULL
select * from #Region
Select COUNT(*) As 'Num of Countries' from #Country
Select COUNT(*) As 'Num of Regions' from #Region

CREATE TABLE CountriesRegions
(
[ID] [int] NOT NULL,
parentid [int] NULL,
segment [nvarchar](50) NULL)
insert into CountriesRegions values (1,null,'usa'), (2,1, 'california'), (3, null, 'canada'), (4, 3, 'quebec'), (5, 1, 'NY')
select a.id, a.segment, count(*) as [Region Count]
from CountriesRegions a
left join CountriesRegions b
on a.id=b.parentid
where b.id is not null
group by a.id, a.segment

Related

SQL Server SELECT first occurrence OR if no occurrence SELECT other criteria

I am having an issue trying to form the proper SQL query for the job here. I have two tables, one is called CUSTOMER and the other is called CUSTOMER_CONTACT. To simplify this, I will only include the relevant column names.
CUSTOMER columns: ID, CUSTOMERNAME
CUSTOMER_CONTACT columns: ID, CUSTOMER_ID, CONTACT_VC, EMAIL
CUSTOMER_ID is the foreign key to link to the CUSTOMER table from CUSTOMER_CONTACT. CONTACT_VC is just the entry number for their contact information. There could be multiple CUSTOMER_CONTACT records for each customer, but they will have a unique CONTACT_VC.
EMAIL can be null/blank on some or all as well.
I need to select the first CUSTOMER_CONTACT entry where EMAIL is NOT NULL/blank but if none of the CUSTOMER_CONTACT entries have an email address, then select CUSTOMER_CONTACT WHERE CONTACT_VC = 1
Any suggestions on how to accomplish this?
The following approach uses ROW_NUMBER to retrieve a number based on your ordering logic within each CUSTOMER_ID group, then filters by the first record retrieved.
You may try the following:
SELECT
*
FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY CUSTOMER_ID
ORDER BY (CASE WHEN EMAIL IS NOT NULL THEN 0 ELSE 1 END),CONTACT_VC
) as rn
FROM
CUSTOMER_CONTACT
) t
WHERE rn=1
If you would like to join this to the customer table you may use the above query as a subquery eg
SELECT
c.*,
contact.*
FROM
CUSTOMER c
INNER JOIN (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY CUSTOMER_ID
ORDER BY (CASE WHEN EMAIL IS NOT NULL THEN 0 ELSE 1 END),CONTACT_VC
) as rn
FROM
CUSTOMER_CONTACT
) contact ON c.ID = contact.CUSTOMER_ID and contact.rn=1
Here is almost the same answer as ggordon, but I used a common table expression and I think the ordering in the subquery portion should go by CONTACT_VS first then by non-NULL email addresses. I created some very simple test data to run this:
DECLARE #CUSTOMER AS TABLE
(
[ID] INT NOT NULL,
[CUSTOMERNAME] VARCHAR(10) NOT NULL
);
INSERT INTO #CUSTOMER
(
[ID],
[CUSTOMERNAME]
)
VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Cathy');
DECLARE #CUSTOMER_CONTACT AS TABLE
(
[ID] INT NOT NULL,
[CUSTOMER_ID] INT NOT NULL,
[CONTACT_VC] INT NOT NULL,
[EMAIL] VARCHAR(40) NULL
);
INSERT INTO #CUSTOMER_CONTACT
(
[ID],
[CUSTOMER_ID],
[CONTACT_VC],
[EMAIL]
)
VALUES
(1, 1, 1, 'alice#email.com'),
(2, 1, 2, 'alice#gmail.com'),
(3, 2, 1, NULL),
(4, 2, 2, 'bob#work.com'),
(5, 3, 1, NULL),
(6, 3, 2, NULL),
(7, 3, 3, NULL);
;WITH [cc]
AS (SELECT [ID],
[CUSTOMER_ID],
[CONTACT_VC],
[EMAIL],
ROW_NUMBER() OVER (PARTITION BY [CUSTOMER_ID]
ORDER BY [CONTACT_VC],
(CASE WHEN [EMAIL] IS NOT NULL THEN
0
ELSE
1
END
)
) AS [rn]
FROM #CUSTOMER_CONTACT)
SELECT [c].[ID], [c].[CUSTOMERNAME], [cc].[ID], [cc].[CUSTOMER_ID], [cc].[CONTACT_VC], [cc].[EMAIL]
FROM #CUSTOMER AS [c]
INNER JOIN [cc]
ON [c].[ID] = [cc].[CUSTOMER_ID]
AND [cc].[rn] = 1;
select * from CUSTOMER_CONTACT where EMAIL IS NOT NULL
union all
select * from CUSTOMER_CONTACT where
(CONTACT_VC=1 and NOT EXISTS (select 1 FROM CUSTOMER_CONTACT where EMAIL IS NOT NUL)
order by CONTACT_VC asc limit 1

Recursively Conditionally get parent Id

I have two tables: Budget Line and Expense. They are structured in such a way that an expense must have either a parent record in the budget line table, or a parent record in the expense table. I need to select all child-most expenses for each budget line.
For example - BudgetLine:
Id
Description
1
TEST123
2
OTHERTEST
Expense:
Id
ParentId
ParentType
Description
1
1
BudgetLine
Group of Expenses
2
1
Expense
Expense # 1
3
1
Expense
Expense # 2
4
2
BudgetLine
Expense 3
Desired result:
BudgetLineId
ExpenseId
Description
1
2
Expense # 1
1
3
Expense # 2
2
4
Expense # 3
I am looking to omit expenses in the result only if they are the only sub-child. Note that an expense may have many children, grandchildren, etc.
I have tried the following, and researching various recursive CTE methods:
WITH RCTE AS
(
SELECT Expense.Id, Expense.ParentId, Expense.ParentType, 1 AS Lvl, Expense.Id as startId FROM Expense
UNION ALL
SELECT rh.Id, rh.ParentId, rh.ParentType, Lvl+1 AS Lvl, rc.Id as startId FROM dbo.Expense rh
INNER JOIN RCTE rc ON rh.Id = rc.ParentId and rc.ParentType = 'Expense'
),
FilteredRCTE AS
(
SELECT startId, MAX(LVL) AS Lvl
FROM RCTE
GROUP BY startID
),
RecursiveData AS
(
SELECT FilteredRCTE.startId AS ExpenseId, RCTE.ParentId AS BudgetLineId
FROM FilteredRCTE
JOIN RCTE ON FilteredRCTE.startId = RCTE.startId AND FilteredRCTE.Lvl = RCTE.Lvl
)
SELECT *
FROM RecursiveData
Which did in-fact obtain all the child Expenses and their associated parent BudgetLine, but it also included the middle-tier expenses (such as item 1 in the example) and I cannot figure out how to filter those middle-tier items out.
Here is a script to create tables / insert sample data:
CREATE TABLE [dbo].[BudgetLine]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[Description] [varchar](500) NULL,
) ON [PRIMARY]
GO
INSERT INTO dbo.BudgetLine VALUES ('TEST123')
INSERT INTO dbo.BudgetLine VALUES ('OTHERTEST')
CREATE TABLE [dbo].[Expense]
(
[Id] [int] IDENTITY(1,1) NOT NULL,
[ParentId] [int] NOT NULL,
[ParentType] [varchar](100) NOT NULL,
[Description] [varchar](max) NULL,
) ON [PRIMARY]
GO
INSERT INTO dbo.Expense VALUES ('1', 'BudgetLine', 'Group of Expenses')
INSERT INTO dbo.Expense VALUES ('1', 'Expense', 'Expense # 1')
INSERT INTO dbo.Expense VALUES ('1', 'Expense', 'Expense # 2')
INSERT INTO dbo.Expense VALUES ('2', 'BudgetLine', 'Expense # 3')
Maybe I have oversimplified, but the following returns your desired results, by checking that there is no other expense row connected to the current row.
WITH RCTE AS
(
SELECT E.Id ExpenseId, E.ParentId, E.ParentType
FROM #Expense E
UNION ALL
SELECT RH.Id, RH.ParentId, RH.ParentType
FROM #Expense RH
INNER JOIN RCTE RC ON RH.Id = RC.ParentId AND RC.ParentType = 'Expense'
)
SELECT *
FROM RCTE R1
WHERE NOT EXISTS (
SELECT 1
FROM RCTE R2
WHERE R2.ParentId = R1.ExpenseId AND R2.ParentType = 'Expense'
);

How to use CHECKSUM in a trigger

I'm trying to create my own logic for tables synchronization in SQL Server Express 2019. I was hoping that such simple task would work:
Have a Customers table
Have a Synchronization table
CREATE TABLE [dbo].[Synchronization]
(
[PK] [uniqueidentifier] NOT NULL,
[TableName] [nchar](50) NOT NULL,
[RecordPK] [uniqueidentifier] NOT NULL,
[RecordChecksum] [int] NOT NULL,
[RecordDate] [datetime] NOT NULL,
[RecordIsDeleted] [bit] NOT NULL
)
Have a trigger on Customers:
CREATE TRIGGER trgCustomers_INSERT
ON Customers
AFTER INSERT
AS
INSERT INTO Synchronization(PK, TableName, RecordPK, RecordChecksum,
RecordDate, RecordIsDeleted)
VALUES (NEWID(), 'Customers',
(SELECT PK FROM inserted),
(SELECT CHECKSUM(*) FROM inserted),
GETDATE(), 0)
... but I got an error about the SELECT CHECKSUM(*) FROM inserted part:
Cannot use CHECKSUM(*) in a computed column, constraint, default definition, or INSERT statement.
Is there any other way to add new Customer's CHECKSUM or some hash to the Synchronization table?
Don't use the VALUES syntax when inserting and you won't get an error using CHECKSUM while inserting.
Example:
declare #t table (val int)
-- works
insert into #t select checksum(*) from ( select ID from (select 1 as ID union select 2) b ) a
-- reproduce error
insert into #t
values
((select top 1 checksum(*) C from ( select ID from (select 1 as ID union select 2) b ) a))
Implementing the concept in your trigger:
CREATE TRIGGER trgCustomers_INSERT
ON Customers
AFTER INSERT
AS
begin
INSERT INTO Synchronization(PK, TableName, RecordPK, RecordChecksum,
RecordDate, RecordIsDeleted)
select NEWID() as PK,
'Customers' as TableName,
PK as RecordPK,
checksum(*) as RecordChecksum,
GETDATE() as RecordDate,
0 as RecordIsDeleted
from inserted
end

SQL Server SQL Statement - Updating record

I have a data as below:
I need to update Matching_id and Matching_Type by using column id, region, company, dept, subdept and amountsepend. The logic is:
Sum AmountSepend by Region, Company, Dept and SubDept. If the sum amount is 0 then Matching_Type is 'Match' and Matching_id is the combination of the id for the matched record else 'Not Match' and Matching_id is the id. **SUM means the total sum of all records for same criteria regardless the AmountSepend is positive or negative.
Another important criteria is if the transaction is single record, meaning the total count by grouping by Region, Company, Dept and SubDept is 1 then Matching type is Not Match and Matching_UID is id regardless the AmountSepend is 0 or positive/negative value. Example id 8.
Below is the output:
Here the table and data script
CREATE TABLE [dbo].[StackoverflowQuest](
[id] [int] NOT NULL,
[Region] [varchar](50) NULL,
[Company] [varchar](50) NULL,
[Dept] [varchar](50) NULL,
[SubDept] [varchar](50) NULL,
[AmountSepend] [float] NULL,
[Matching_id] [varchar](100) NULL,
[Matching_Type] [varchar](100) NULL
) ON [PRIMARY]
How could I achieved such result ? Any help/hint would be appreciate
CREATE TABLE #Table(Id INT,Region VARCHAR(100),Company INT,Dept INT,SubDept
INT,AmtSpend INT,MatchingId VARCHAR(100),MatchingType VARCHAR(100))
INSERT INTO #Table(Id ,Region ,Company , Dept ,SubDept ,AmtSpend )
SELECT 1,'NAM',12378,1,NULL,900 UNION ALL
SELECT 2,'NAM',12378,1,NULL,-900 UNION ALL
SELECT 3,'NAM',12370,1,23,1000 UNION ALL
SELECT 4,'ASA',1234,9,12,5000 UNION ALL
SELECT 5,'NAM',12370,1,23,-1000 UNION ALL
SELECT 6,'ASA',1234,9,12,800 UNION ALL
SELECT 7,'ASA',1234,9,12,-600 UNION ALL
SELECT 8,'ASA',12311,6,NULL,200
UPDATE #Table SET MatchingId = MatchIds,MatchingType = 'Match'
FROM
(
SELECT T2.Company,STUFF( ( SELECT ',' + CAST(T3.Id AS VARCHAR) FROM #Table
T3 WHERE T2.Company = T3.Company FOR XML PATH('')),1,1,'') MatchIds
FROM #Table T2
JOIN
(
SELECT T1.Company Company,SUM(T1.AmtSpend) Total
FROM #Table T1
GROUP BY T1.Company
HAVING SUM(T1.AmtSpend) = 0
)A ON A.Company = T2.Company
GROUP BY T2.Company
) A
WHERE A.Company = #Table.Company
UPDATE #Table SET MatchingId = CAST(Id AS VARCHAR),MatchingType = 'Not
Match' WHERE ISNULL(MatchingId,'') = ''
SELECT * FROM #Table

SQL Count returns wrong value in my INNER JOIN Statement

I am quite new to SQL and I am having some difficulty with my COUNT() feature.
It keeps returning the wrong COUNT() value as I am trying to calculate the TOTAL number of specific cars sold by users. My tables and results are here:
http://sqlfiddle.com/#!9/d2ef0/5
My Schema:
CREATE TABLE IF NOT EXISTS Users(
userID INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
forename VARCHAR(50) NOT NULL,
surname VARCHAR(50) NOT NULL,
PRIMARY KEY (userID)
);
CREATE TABLE IF NOT EXISTS CarType(
carTypeID INT NOT NULL AUTO_INCREMENT,
description VARCHAR(80),
PRIMARY KEY (carTypeID)
);
CREATE TABLE IF NOT EXISTS Country(
countryID INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
PRIMARY KEY (countryID)
);
CREATE TABLE IF NOT EXISTS Cars(
carID INT NOT NULL AUTO_INCREMENT,
carTypeID INT NOT NULL,
countryID INT NOT NULL,
description VARCHAR(100) NOT NULL,
make VARCHAR(100) NOT NULL,
model VARCHAR(100),
FOREIGN KEY (carTypeID) REFERENCES CarType(carTypeID),
FOREIGN KEY (countryID) REFERENCES Country(countryID),
PRIMARY KEY (carID)
);
CREATE TABLE IF NOT EXISTS Likes(
userID INT NOT NULL,
carID INT NOT NULL,
likes DOUBLE NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
CREATE TABLE IF NOT EXISTS Sold(
userID INT NOT NULL,
carID INT NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
INSERT INTO Users VALUES
(NULL, "micheal", "Micheal", "Sco"),
(NULL, "bensco", "Ben", "Sco"),
(NULL, "shanemill", "Shane", "Miller");
INSERT INTO CarType VALUES
(NULL, "Saloon"),
(NULL, "HatchBack"),
(NULL, "Low Rider");
INSERT INTO Country VALUES
(NULL, "UK"),
(NULL, "USA"),
(NULL, "JAPAN"),
(NULL, "GERMANY");
INSERT INTO Cars VALUES
(NULL, 1, 2, "Ford Mustang lovers", "Mustang", "Ford"),
(NULL, 2, 3, "Drift Kings", "Skyline", "Nissan"),
(NULL, 3, 1, "British classic", "Cooper", "Mini");
INSERT INTO Likes VALUES
(1, 1, 3),
(1, 2, 2),
(2, 3, 5),
(2, 3, 7),
(2, 3, 1),
(2, 3, 2);
INSERT INTO Sold VALUES
(1, 2),
(1, 3),
(1, 1),
(2, 2),
(2, 3),
(3, 1),
(3, 3);
This is the Sold table:
userID carID
1 2
1 3
1 1
2 2
2 3
3 1
3 3
This is my complex query:
SELECT DISTINCT Cars.carID, Cars.description, Cars.model, Country.name,
CarType.description, ROUND(AVG(Likes.likes)), COUNT(*)
FROM Cars
INNER JOIN Sold ON
Cars.carID = Sold.carID
INNER JOIN Country ON
Cars.countryID = Country.countryID
INNER JOIN CarType ON
Cars.carTypeID = CarType.carTypeID
INNER JOIN Likes ON
Cars.carID = Likes.carID
GROUP BY Cars.carID
The actual result from this complex SQL Query:
carID description model name description ROUND(AVG(Likes.likes)) COUNT(*)
1 Ford Mustang lovers Ford USA Saloon 3 2
2 Drift Kings Nissan JAPAN HatchBack 2 2
3 British classic Mini UK Low Rider 4 12
For example, the result for the last one is incorrect - it should not be 12
Would be nice if someone could tell me where I went wrong
Thanks
You are attempting to aggregate across two different dimensions -- Sold and Likes. The result is a Cartesian product of rows for each car, and this throws off the aggregations.
The solution is the pre-aggregate the results along each dimension:
SELECT c.carID, c.description, c.model, cy.name, ct.description,
l.avgLikes, s.NumSold
FROM Cars c INNER JOIN
(SELECT s.CarId, COUNT(*) as NumSold
FROM Sold s
GROUP BY s.CarId
) s
ON c.carID = s.carID INNER JOIN
Country cy
ON c.countryID = cy.countryID INNER JOIN
CarType ct
ON c.carTypeID = ct.carTypeID LEFT JOIN
(SELECT l.carId, AVG(Likes) as avgLikes
FROM Likes l
GROUP BY CarId
) l
ON c.carID = l.carID;
Here is the SQL Fiddle.
If all you want is the total number of specific cars sold by user then all your info is in the sold table. This query will give you what you want by carID. You can use that as a subquery if you want to join on other tables to get more info.
SELECT userID, carID, count(*) as totalSold FROM Sold GROUP BY userID, carID;