Querying multiple transactions into a single line - sql

I am trying to create a small home finance app. I am trying a sort of Double Entry type design, but really battling to find a way to efficialy generate a staement.
So I have created a dummy script that I am using to test. What I have is:
Accounts table (entities that I can pay to, and from).
Budgets table (budgets that I can assign expenses to, so that I can allocate portions of transactions to them. Note that a transaction can be split amoungst different budgets, as an example I have provided)
Transaction table (Holding the header info for a transaction)
TransactionLine table (A breakdown of the transaction, including amount and account it comes from)
What I am then trying to do, is achieve the following:
I need to efficiently present data to render a statement. So, given an accountID, I want to see the transactions.
But because TransactionLines are split onto multiple lines, I'm finding it hard to get the data in a single line:
Date-- Who I paid or recieved money from in this transaction -- the amount -- If it's a debit or credit.
So the raw data I have to work with:
And then I am trying to break that down to:
So I have mocked up the data, and tried to explain what I need. The script uses table variables, so is re-runnable.
DECLARE #Account TABLE (
Id INT NOT NULL,
Name VARCHAR(20)
)
INSERT INTO #Account VALUES (1, 'My Bank Account')
INSERT INTO #Account VALUES (2, 'My Work')
INSERT INTO #Account VALUES (3, 'A restaurant')
INSERT INTO #Account VALUES (4, 'A coffee shop')
INSERT INTO #Account VALUES (5, 'A Department Store')
DECLARE #Budget TABLE (
Id INT NOT NULL,
Name VARCHAR(20)
)
INSERT INTO #Budget VALUES (1, 'My Budget')
INSERT INTO #Budget VALUES (2, 'My Clothing Budget')
DECLARE #Transaction TABLE (
Id INT NOT NULL ,
Date DATETIME NOT NULL,
Description VARCHAR(20)
)
DECLARE #TransactionLine TABLE (
Id INT NOT NULL,
TransactionId INT NOT NULL,
AccountId INT,
BudgetId INT NULL,
DebitAmount DECIMAL NOT NULL,
CreditAmount DECIMAL NOT NULL
)
-- Got paid, from My Work to My Account
INSERT INTO #Transaction VALUES (1, GETUTCDATE(), 'Got Paid')
INSERT INTO #TransactionLine VALUES (1, 1, 1, NULL, 0, 1000) -- Credit My Bank ccount
INSERT INTO #TransactionLine VALUES (2, 1, 2, NULL, 1000, 0) -- Debit My Work
-- Got a coffee, from My Account to A Coffee Shop
INSERT INTO #Transaction VALUES (2, GETUTCDATE(), 'Got a Coffee')
INSERT INTO #TransactionLine VALUES (3, 2, 1, NULL, 5, 0) -- Debit My Account
INSERT INTO #TransactionLine VALUES (4, 2, 4, NULL, 0, 5) -- Credit a Coffee shop
-- Went to dinner, from My Account to A restaurant. This comes off My Budget
INSERT INTO #Transaction VALUES (3, GETUTCDATE(), 'Went to Dinner')
INSERT INTO #TransactionLine VALUES (5, 3, 1, 1, 25, 0) -- Debit My Account
INSERT INTO #TransactionLine VALUES (6, 3, 3, NULL, 0, 25) -- Credit A restaurant
INSERT INTO #Transaction VALUES (4, GETUTCDATE(), 'Did weekly shopping')
INSERT INTO #TransactionLine VALUES (9, 4, 1, 1, 25, 0) -- Debit My Account with 25, and assign it to My Budget
INSERT INTO #TransactionLine VALUES (9, 4, 1, 2, 75, 0) -- Debit My Account with 75, and assign it to My Clothing Budget
INSERT INTO #TransactionLine VALUES (11, 4, 5, NULL, 0, 50) -- Credit tghe Department store with 100
-- View the raw data.
SELECT t.id, Date, Description, AccountId, a.Name as AccountName, DebitAmount, CreditAmount, b.Name as BudgetName
FROM #Transaction t
INNER JOIN #TransactionLine tl
ON tl.TransactionId = t.Id
INNER JOIN #Account a
ON a.id = tl.AccountId
LEFT JOIN #Budget b
ON b.id = tl.BudgetId
-- View the raw data based on a select Account ID. i.e. I'm viewing a statement for 'My Bank Account'
SELECT t.id, Date, Description, AccountId, a.Name as AccountName, DebitAmount, CreditAmount, b.Name as BudgetName
FROM #Transaction t
INNER JOIN #TransactionLine tl
ON tl.TransactionId = t.Id
INNER JOIN #Account a
ON a.id = tl.AccountId
LEFT JOIN #Budget b
ON b.id = tl.BudgetId
WHERE AccountId = 1
-- Need to get:
SELECT 1 AS Id, GETUTCDATE() AS Date, 'Got Paid' AS Description, 1 AS AccountId, 'My Bank Account' as AccountName, 'My Work' AS OtherAccountName, 'Credit' as Type, 1000 as Amount, NULL AS Budget
UNION
SELECT 2 AS Id, GETUTCDATE() AS Date, 'Got a Coffee' AS Description, 1 AS AccountId, 'My Bank Account' as AccountName, 'A coffee shop' AS OtherAccountName, 'Debit' as Type, 5 as Amount, NULL AS Budget
UNION
SELECT 3 AS Id, GETUTCDATE() AS Date, 'Went to Dinner' AS Description, 1 AS AccountId, 'My Bank Account' as AccountName, 'A restaurant' AS OtherAccountName, 'Debit' as Type, 25 as Amount, 'My Budget' AS Budget
UNION
SELECT 4 AS Id, GETUTCDATE() AS Date, 'Did weekly shopping' AS Description, 1 AS AccountId, 'My Bank Account' as AccountName, 'A Department Store' AS OtherAccountName, 'Debit' as Type, 100 as Amount, '* Multiple Mudgets' AS Budget
-- So that I can create a statement fro My Bank Account.
SELECT '2019-07-28 My Work +1000' UNION
SELECT '2019-07-28 A coffee shop -5' UNION
SELECT '2019-07-28 A restaurant* -25' UNION
SELECT '2019-07-28 A department Store* -100'
-- Where the * in the description indicates it has a Budget assigned.
The main issue I find is: given I have an AccountID, I can find the transactions related to that account, but ... how do I get the OTHER account to which the transaction had an effect.

Firstly, well structured question and thank you for the data. Secondly, you're after window functions to solve your problem. Take special note of the ROWS BETWEEN section.
Please note that you'll usually see people declare a CTE like this (which you shouldn't do):
;with cteFooBar AS
This is because a CTE must come after a semi-colon, so make sure you put a ; on the end of all your statements so the below works:
With transactions AS
(
SELECT t.id, Date, Description, AccountId, a.Name as AccountName, DebitAmount, CreditAmount,
IIF(
COUNT(1) OVER (PARTITION BY Description ORDER BY [Date] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) > 1, 'Multiple Budgets', b.Name) as BudgetName
FROM #Transaction t
INNER JOIN #TransactionLine tl ON tl.TransactionId = t.Id
INNER JOIN #Account a ON a.id = tl.AccountId
LEFT JOIN #Budget b ON b.id = tl.BudgetId
WHERE a.Name = 'My Bank Account'
AND AccountId = 1
)
SELECT id, Date, Description, AccountId, AccountName, SUM(DebitAmount) [DebitAmount], SUM(CreditAmount) [CreditAmount], BudgetName
FROM transactions
GROUP BY id, Date, Description, AccountId, AccountName, BudgetName;
DECLARE #AccountName VARCHAR(50) = 'A Department Store';
With myAccount AS
(
SELECT t.id, Date, Description, AccountId, a.Name as AccountName, DebitAmount, CreditAmount
FROM #Transaction t
INNER JOIN #TransactionLine tl ON tl.TransactionId = t.Id
INNER JOIN #Account a ON a.id = tl.AccountId
LEFT JOIN #Budget b ON b.id = tl.BudgetId
)
SELECT CAST(Date AS DATE) [Date], AccountName, IIF(SUM(-DebitAmount + CreditAmount) > 0, '-','+') + CAST(SUM(DebitAmount + CreditAmount) AS VARCHAR(1000)) [Amount]
FROM myAccount
WHERE (AccountName = #AccountName OR
(
#AccountName IS NULL AND AccountName != 'My Bank Account'
))
GROUP BY CAST(Date AS DATE), Description, AccountName
ORDER BY AccountName;

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

SQL Duplicates optimization

I have the following query:
Original query:
SELECT
cd1.cust_number_id, cd1.cust_number_id, cd1.First_Name, cd1.Last_Name
FROM #Customer_Data cd1
inner join #Customer_Data cd2 on
cd1.Cd_Id <> cd2.Cd_Id
and cd2.cust_number_id <> cd1.cust_number_id
and cd2.First_Name = cd1.First_Name
and cd2.Last_Name = cd1.Last_Name
inner join #Customer c1 on c1.Cust_id = cd1.cust_number_id
inner join #Customer c2 on c2.cust_id = cd2.cust_number_id
WHERE c1.cust_number <> c2.cust_number
I optimized it as follows, but there is an error in my optimization and I can't find it:
Optimized query:
SELECT cd1.cust_number_id, cd1.cust_number_id, cd1.First_Name,cd1.Last_Name
FROM (
SELECT cdResult.cust_number_id, cdResult.First_Name,cdResult.Last_Name, COUNT(*) OVER (PARTITION BY cdResult.First_Name, cdResult.Last_Name) as cnt_name_bday
FROM #Customer_Data cdResult
WHERE cdResult.First_Name IS NOT NULL
AND cdResult.Last_Name IS NOT NULL) AS cd1
WHERE cd1.cnt_name_bday > 1;
Test data:
DECLARE #Customer_Data TABLE
(
Cd_Id INT,
cust_number_id INT,
First_Name NVARCHAR(30),
Last_Name NVARCHAR(30)
)
INSERT #Customer_Data (Cd_Id,cust_number_id,First_Name,Last_Name)
VALUES (1, 22, N'Alex', N'Bor'),
(2, 22, N'Alex', N'Bor'),
(3, 23, N'Alex', N'Bor'),
(4, 24, N'Tom', N'Cruse'),
(5, 25, N'Tom', N'Cruse')
DECLARE #Customer TABLE
(
Cust_id INT,
Cust_number INT
)
INSERT #Customer (Cust_id, Cust_number)
VALUES (22, 022),
(23, 023),
(24, 024),
(25, 025)
The problem is that the original query returns 6 rows (duplicating the row). And optimized returns just duplicates, how to make the optimized query also duplicated the row?
I would suggest just using window functions:
SELECT CD.cud_customer_id
FROM (SELECT cd.*, COUNT(*) OVER (PARTITION BY cud_name, cud_birthday) as cnt_name_bday FROM dbo.customer_data cd
) cd
WHERE cnt_name_bday > 1;
Your query is finding duplicates for either name or birthday. You want duplicates with both at the same time.
You can use only one exists :
SELECT cd.cud_customer_id
FROM dbo.customer_data AS cd
WHERE EXISTS (SELECT 1
FROM dbo.customer_data AS c
WHERE c.cud_name = cd.cud_name AND c.cud_birthday = cd.cud_birthday AND c.cust_id <> cd.cud_customer_id
);

SQL Update Or Insert By Comparing Dates

I am trying to do the UPDATE or INSERT, but I am not sure if this is possible without using loop. Here is the example:
Says, I have this SQL below in which I joined two tables: tblCompany and tblOrders.
SELECT CompanyID, CompanyName, c.LastSaleDate, o.SalesOrderID, o.SalesPrice
, DATEADD(m, -6, GETDATE()) AS DateLast6MonthFromToday
FROM dbo.tblCompany c
CROSS APPLY (
SELECT TOP 1 SalesOrderID, SalesPrice
FROM dbo.tblOrders o
WHERE c.CompanyID = o.CompanyID
ORDER BY SalesOrderID DESC
) AS a
WHERE Type = 'End-User'
Sample Result:
CompanyID, SalesOrderID, SalesPrice, LastSalesDate, DateLast6MonthFromToday
101 10001 50 2/01/2016 10/20/2016
102 10002 80 12/01/2016 10/20/2016
103 10003 80 5/01/2016 10/20/2016
What I am trying to do is comparing the LastSalesDate and the DateLast6MonthFromToday. Condition is below:
If the LastSalesDate is lesser (earlier), then do the INSERT INTO tblOrders (CompanyID, Column1, Column2...) VALUES (CompanyIDFromQuery, Column1Value, Column2Value)
Else, do UPDATE tblOrders SET SalesPrice = 1111 WHERE SalesOrderID = a.SalesOrderID
As the above sample result, the query will only update SalesOrderID 10001 and 10003. And For Company 102, NO insert since the LastSaleDate is greater, then just do the UPDATE for the SalesOrderID.
I know it is probably can be done if I create a Cursor to loop through every record and do the comparison then Update or Insert, but I wonder if there is another way perform this without the loop since I have around 20K records.
Sorry for the confusion,
I don't know your tables structure and your data types. Also I know nothing
about duplicates and join ralationships between this 2 tables.
But I want only show how it works on next example:
use [your test db];
go
create table dbo.tblCompany
(
companyid int,
companyname varchar(max),
lastsaledate datetime,
[type] varchar(max)
);
create table dbo.tblOrders
(
CompanyID int,
SalesOrderID int,
SalesPrice float
);
insert into dbo.tblCompany
values
(1, 'Avito', '2016-01-01', 'End-User'),
(2, 'BMW', '2016-05-01', 'End-User'),
(3, 'PornHub', '2017-01-01', 'End-User')
insert into dbo.tblOrders
values
(1, 1, 500),
(1, 2, 700),
(1, 3, 900),
(2, 1, 500),
(2, 2, 700),
(2, 3, 900),
(3, 1, 500),
(3, 2, 700),
(3, 3, 900)
declare #column_1_value int = 5;
declare #column_2_value int = 777;
with cte as (
select
CompanyID,
SalesOrderID,
SalesPrice
from (
select
CompanyID,
SalesOrderID,
SalesPrice,
row_number() over(partition by CompanyID order by SalesOrderId desc) as rn
from
dbo.tblOrders
) t
where rn = 1
)
merge cte as target
using (select * from dbo.tblCompany where [type] = 'End-User') as source
on target.companyid = source.companyid
and source.lastsaledate >= dateadd(month, -6, getdate())
when matched
then update set target.salesprice = 1111
when not matched
then insert (
CompanyID,
SalesOrderID,
SalesPrice
)
values (
source.CompanyId,
#column_1_value,
#column_2_value
);
select * from dbo.tblOrders
If you will give me an information, then I can prepare target and source tables properly.

SQL Joining on Field with Nulls

I'm trying to match two tables where one of the tables stores multiple values as a string.
In the example below I need to classify each product ordered from the #Orders table with a #NewProduct.NewProductId.
The issue I'm having is sometimes we launch a new product like "Black Shirt",
then later we launch an adaption to that product like "Black Shirt Vneck".
I need to match both changes correctly to the #Orders table. So if the order has Black and Shirt, but not Vneck, it's considered a "Black Shirt", but if the order has Black and Shirt and Vneck, it's considered a "Black Vneck Shirt."
The code below is an example - the current logic I'm using returns duplicates with the Left Join.
Also, assume we can modify the format of #NewProducts but not #Orders.
IF OBJECT_ID('tempdb.dbo.#NewProducts') IS NOT NULL DROP TABLE #NewProducts
CREATE TABLE #NewProducts
(
ProductType VARCHAR(MAX)
, Attribute_1 VARCHAR(MAX)
, Attribute_2 VARCHAR(MAX)
, NewProductId INT
)
INSERT #NewProducts
VALUES
('shirt', 'black', 'NULL', 1),
('shirt', 'black', 'vneck', 2),
('shirt', 'white', 'NULL', 3)
IF OBJECT_ID('tempdb.dbo.#Orders') IS NOT NULL DROP TABLE #Orders
CREATE TABLE #Orders
(
OrderId INT
, ProductType VARCHAR(MAX)
, Attributes VARCHAR(MAX)
)
INSERT #Orders
VALUES
(1, 'shirt', 'black small circleneck'),
(2, 'shirt', 'black large circleneck'),
(3, 'shirt', 'black small vneck'),
(4, 'shirt', 'black small vneck'),
(5, 'shirt', 'white large circleneck'),
(6, 'shirt', 'white small vneck')
SELECT *
FROM #Orders o
LEFT JOIN #NewProducts np
ON o.ProductType = np.ProductType
AND CHARINDEX(np.Attribute_1, o.Attributes) > 0
AND (
CHARINDEX(np.Attribute_2, o.Attributes) > 0
OR np.Attribute_2 = 'NULL'
)
You seem to want the longest overlap:
SELECT *
FROM #Orders o OUTER APPLY
(SELECT Top (1) np.*
FROM #NewProducts np
WHERE o.ProductType = np.ProductType AND
CHARINDEX(np.Attribute_1, o.Attributes) > 0
ORDER BY ((CASE WHEN CHARINDEX(np.Attribute_1, o.Attributes) > 0 THEN 1 ELSE 0 END) +
(CASE WHEN CHARINDEX(np.Attribute_2, o.Attributes) > 0 THEN 1 ELSE 0 END)
) DESC
) np;
I can't say I'm thrilled with the need to do this. It seems like the Orders should contain numeric ids that reference the actual product. However, I can see how something like this is sometimes necessary.
I couldn't get Gordon's answer to work, and was part way through my own response when his came in. His idea of taking the biggest overlap helped. I've tweaked your NewProducts table, so that that side of things is "normalised" even if the Orders table cannot be. Code below or at rextester.com/ERIF13021
create table #NewProduct
(
NewProductID int primary key,
ProductType varchar(max),
ProductName varchar(max)
)
create table #Attribute
(
AttributeID int primary key,
AttributeName varchar(max)
)
create table #ProductAttribute
(
NewProductID int,
AttributeID int
)
insert into #NewProduct
values (1, 'shirt', 'black shirt'),
(2, 'shirt', 'black vneck shirt'),
(3, 'shirt', 'white shirt')
insert into #Attribute
values (1, 'black'),
(2, 'white'),
(3, 'vneck')
insert into #ProductAttribute
values (1,1),
(2,1),
(2,3),
(3,2)
select top 1 with ties
*
from
(
select
o.OrderId,
p.NewProductID,
p.ProductType,
p.ProductName,
o.Attributes,
sum(case when charindex(a.AttributeName,o.Attributes)>0 then 1 else 0 end) as Matches
from
#Orders o
JOIN #Attribute a ON
charindex(a.AttributeName,o.Attributes)>0
JOIN #ProductAttribute pa ON
a.AttributeID = pa.AttributeID
JOIN #NewProduct p ON
pa.NewProductID = p.NewProductID AND
o.ProductType = p.ProductType
group by
o.OrderId,
p.NewProductID,
p.ProductType,
p.ProductName,
o.Attributes
) o2
order by
row_number() over (partition by o2.OrderID order by o2.Matches desc)

SQL Stored procedure to obtain top customers

I'm trying to create a stored procedure that goes through a "SALES" table and returns the best two customers of a pharmacy (the two customers who have spent more money).
Here's some code:
Table creation:
create table Customer (
Id_customer int identity(1,1) Primary Key,
Name varchar(30),
Address varchar(30),
DOB datetime,
ID_number int not null check (ID_number > 0),
Contributor int not null check (Contributor > 0),
Customer_number int not null check (Customer_number > 0)
)
create table Sale (
Id_sale int identity(1,1) Primary Key,
Id_customer int not null references Customer(Id_customer),
Sale_date datetime,
total_without_tax money,
total_with_tax money
)
Well, I don't know if this is useful but I have a function that returns the total amount spent by a customer as long as I provide the customer's ID.
Here it is:
CREATE FUNCTION [dbo].[fGetTotalSpent]
(
#Id_customer int
)
RETURNS money
AS
BEGIN
declare #total money
set #total = (select sum(total_with_tax) as 'Total Spent' from Sale where Id_customer=#Id_customer)
return #total
END
Can someone help me get the two top customers?
Thanks
Chiapa
PS: Here's some data to insert so you can test it better:
insert into customer values ('Jack', 'Big street', '1975.02.01', 123456789, 123456789, 2234567891)
insert into customer values ('Jim', 'Little street', '1985.02.01', 223456789, 223456789, 2234567891)
insert into customer values ('John', 'Large street', '1977.02.01', 323456789, 323456789, 3234567891)
insert into customer values ('Jenny', 'Huge street', '1979.02.01', 423456789, 423456789, 4234567891)
insert into sale values (1, '2013.04.30', null, 20)
insert into sale values (2, '2013.05.22', null, 10)
insert into sale values (3, '2013.03.29', null, 30)
insert into sale values (1, '2013.05.19', null, 34)
insert into sale values (1, '2013.06.04', null, 21)
insert into sale values (2, '2013.06.01', null, 10)
insert into sale values (2, '2013.05.08', null, 26)
You can do this with a single query without any special functions:
select top 2 c.id_customer, c.name, sum(s.total_with_tax)
from customer c
join sale s on c.id_customer = s.id_customer
group by c.id_customer, c.name
order by sum(s.total_with_tax) desc
This joins onto a CTE with the top customers.
Remove the WITH TIES option if you want exactly 2 and don't want to include customers tied with the same spend.
WITH Top2
AS (SELECT TOP 2 WITH TIES Id_customer,
SUM(total_with_tax) AS total_with_tax
FROM Sale
GROUP BY Id_customer
ORDER BY SUM(total_with_tax) DESC)
SELECT *
FROM Customer C
JOIN Top2 T
ON C.Id_customer = T.Id_customer
I'm not really into SQL Server dialect, but this one will give you best customers in descending order along with money they spent:
select Id_customer, total_with_tax from
(select Id_customer, sum(total_with_tax) total_with_tax from Sale group by Id_customer)
order by total_with_tax desc