Truncate in SQL Server rounding values - sql-server

I'm looking for a way to truncate or drop extra decimal places in SQL. I've found a way but i'm having a problem with values that do not have 3 decimal places.
I have the following data
ProductID | Price | Amount
------------+----------+---------
100 | 50.01 | 1
101 | 25 | 0.789
It's very simple, all I need to do is get the total from each product (Price * Amount).
My query:
select
[ProductID],
[Price],
[Amount],
round(SUM(([Price] * [Amount])),2,1) as 'Total'
from
[Tables]
What I get is:
ProductID | Price | Amount | Total
-----------+-----------+-----------+-----------
100 | 50.01 | 1 | 50 <=======
101 | 25 | 0.789 | 19.72
So, if my calculator is working, the result of this simple operation is:
(50.01 * 1) = 50.01
-
(25 * 0.789) = 19.725
-
Question: SQL does the trick dropping the 5 from the 19.725, but why does (50.01 * 1) equals 50?
I do know that if I use Round((value),2,0) I'll get 50.01, but if I do that 19.725 becomes 19.73 and that is not correct for my application.
What can I do to fix this?

If you cast price and amount to either numeric or decimal data type as shown below, you should arrive at the expected result:
DECLARE #Tables table
(
ProductID int,
Price float,
Amount float
);
INSERT #Tables
(ProductID, Price, Amount)
VALUES
(100, 50.01, 1),
(101, 25, 0.789);
SELECT ProductID
,Price
,Amount
,ROUND(SUM((CAST(Price AS decimal(5,2)) * CAST(Amount AS decimal(5,3)))),2,1) AS 'Total'
FROM #Tables
GROUP BY ProductID, Price, Amount;
(2 row(s) affected)
ProductID Price Amount Total
----------- ---------------------- ---------------------- ---------------------------------------
100 50.01 1 50.01000
101 25 0.789 19.72000
(2 row(s) affected)

SELECT ProductID,
Price,
Amount,
CAST(SUBSTRING(CAST(CAST(Price * Amount AS decimal(18,3)) AS VARCHAR),0, LEN(CAST(CAST(Price * Amount AS decimal(18,3)) AS VARCHAR))) AS DECIMAL(18,2)) AS Total
FROM [Tables]

Related

Extract Quantity and price from text

I have these data
CREATE TABLE #Items (ID INT , Col VARCHAR(300))
INSERT INTO #Items VALUES
(1, 'Dave sold 10 items are sold to ABC servercies at 2.50 each'),
(2, '21 was sold to Tray Limited 3.90 each'),
(3, 'Consulting ordered 15 at 7.11 per one'),
(4, 'Returns from Murphy 7 at a cost of 6.10 for each item')
from the Col i want to extract Quantity and Price
I have written the below query which extract the quantity
SELECT
ID,
Col,
LEFT(SUBSTRING(Col, PATINDEX('%[0-9]%', Col), LEN(Col)),2) AS Qty
FROM #Items
my difficulty is that i don't how i can extract the Pice.
Expected output
You were told already, that storing values within such a string is a real no-no-go.
But - if you have to deal with external input - you might try this:
DECLARE #items TABLE(ID INT , Col VARCHAR(300))
INSERT INTO #items VALUES
(1, 'Dave sold 10 items are sold to ABC servercies at 2.50 each'),
(2, '21 was sold to Tray Limited 3.90 each'),
(3, 'Consulting ordered 15 at 7.11 per one'),
(4, 'Returns from Murphy 7 at a cost of 6.10 for each item');
SELECT i.ID
,i.Col
,A.Casted.value('/x[not(empty(. cast as xs:int?))][1]','int') AS firstNumberAsInt
,A.Casted.value('/x[not(empty(. cast as xs:decimal?))][2]','decimal(10,4)') AS SecondNumberAsDecimal
FROM #items i
CROSS APPLY(SELECT CAST('<x>' + REPLACE((SELECT i.Col AS [*] FOR XML PATH('')),' ','</x><x>') + '</x>' AS XML)) A(Casted);
The idea in short:
we use some string methods to transform your string into XML, where each word is within it's own <x>-element.
We use XML-XQuery's abilities to pick only nodes which answer a predicate.
We use the predicate not(empty(. cast as someType)). This will return an element only in cases, where its content can be casted. Any other element is omitted.
The result:
+----+------------------------------------------------------------+------------------+-----------------------+
| ID | Col | firstNumberAsInt | SecondNumberAsDecimal |
+----+------------------------------------------------------------+------------------+-----------------------+
| 1 | Dave sold 10 items are sold to ABC servercies at 2.50 each | 10 | 2.5000 |
+----+------------------------------------------------------------+------------------+-----------------------+
| 2 | 21 was sold to Tray Limited 3.90 each | 21 | 3.9000 |
+----+------------------------------------------------------------+------------------+-----------------------+
| 3 | Consulting ordered 15 at 7.11 per one | 15 | 7.1100 |
+----+------------------------------------------------------------+------------------+-----------------------+
| 4 | Returns from Murphy 7 at a cost of 6.10 for each item | 7 | 6.1000 |
+----+------------------------------------------------------------+------------------+-----------------------+
I'm sure you know that there are millions of cases where this kind of parsing will break...
First things first: DON'T store things like that in a DB and expect to be able just "extract" data. I can give you a solution given the data you have, but it's going to fall down pretty quickly if anyone enters something silly, for example "Sold ice creams 1.50 each x 10" or "Bought 5 sorbets total 20".
What we will do is use CROSS APPLY in series to calculate the positions of each number.
SELECT
ID,
Col,
CAST(SUBSTRING(Col, FirstNum, EndFirst - 1) AS int) AS Qty,
CAST(SUBSTRING(Col, FirstNum + EndFirst + SecondNum - 2, EndSecond) AS decimal(18,2)) AS Price
FROM #Items
CROSS APPLY (VALUES (PATINDEX('%[0-9]%', Col) ) ) v1(FirstNum)
CROSS APPLY (VALUES (PATINDEX('%[^0-9]%', SUBSTRING(Col, FirstNum, LEN(Col))) ) ) v2(EndFirst)
CROSS APPLY (VALUES (PATINDEX('%[0-9.]%', SUBSTRING(Col, FirstNum + EndFirst - 1, LEN(Col))) ) ) v3(SecondNum)
CROSS APPLY (VALUES (PATINDEX('%[^0-9.]%', SUBSTRING(Col, FirstNum + EndFirst - 1 + SecondNum, LEN(Col))) ) ) v4(EndSecond)

Insert multuple rows at once with a calculated column from prior inserts into SQL Server

I'm trying to figure out how to do a multi-row insert as one statement in SQL Server, but where one of the columns is a column computer based on the data as it stands after every insert row.
Let's say I run this simple query and get back 3 records:
SELECT *
FROM event_courses
WHERE event_id = 100
Results:
id | event_id | course_id | course_priority
---+----------+-----------+----------------
10 | 100 | 501 | 1
11 | 100 | 502 | 2
12 | 100 | 503 | 3
Now I want to insert 3 more records into this table, except I need to be able to calculate the priority for each record. The priority should be the count of all courses in this event. But if I run a sub-query, I get the same priority for all new courses:
INSERT INTO event_courses (event_id, course_id, course_priority)
VALUES (100, 500,
(SELECT COUNT (id) + 1 AS cnt_event_courses
FROM event_courses
WHERE event_id = 100)),
(100, 501,
(SELECT COUNT (id) + 1 AS cnt_event_courses
FROM event_courses
WHERE event_id = 1))
Results:
id | event_id | course_id | course_priority
---+----------+-----------+-----------------
10 | 100 | 501 | 1
11 | 100 | 502 | 2
12 | 100 | 503 | 3
13 | 100 | 504 | 4
14 | 100 | 505 | 4
15 | 100 | 506 | 4
Now I know I could easily do this in a loop outside of SQL and just run a bunch of insert statement, but that's not very efficient. There's got to be a way to calculate the priority on the fly during a multi-row insert.
Big thanks to #Sean Lange for the answer. I was able to simplify it even further for my application. Great lead! Learned 2 new syntax tricks today ;)
DECLARE #eventid int = 100
INSERT event_courses
SELECT #eventid AS event_id,
course_id,
course_priority = existingEventCourses.prioritySeed + ROW_NUMBER() OVER(ORDER BY tempid)
FROM (VALUES
(1, 501),
(2, 502),
(3, 503)
) courseInserts (tempid, course_id) -- This basically creates a temp table in memory at run-time
CROSS APPLY (
SELECT COUNT(id) AS prioritySeed
FROM event_courses
WHERE event_id = #eventid
) existingEventCourses
SELECT *
FROM event_courses
WHERE event_id = #eventid
Here is an example of how you might be able to do this. I have no idea where your new rows values are coming from so I just tossed them in a derived table. I doubt your final solution would look like this but it demonstrates how you can leverage ROW_NUMBER for accomplish this type of thing.
declare #EventCourse table
(
id int identity
, event_id int
, course_id int
, course_priority int
)
insert #EventCourse values
(100, 501, 1)
,(100, 502, 2)
,(100, 503, 3)
select *
from #EventCourse
insert #EventCourse
(
event_id
, course_id
, course_priority
)
select x.eventID
, x.coursePriority
, NewPriority = y.MaxPriority + ROW_NUMBER() over(partition by x.eventID order by x.coursePriority)
from
(
values(100, 504)
,(100, 505)
,(100, 506)
)x(eventID, coursePriority)
cross apply
(
select max(course_priority) as MaxPriority
from #EventCourse ec
where ec.event_id = x.eventID
) y
select *
from #EventCourse

SQL Server SUM based on subsequent records

Microsoft SQL Server 2012 (SP1) - 11.0.3156.0 (X64)
I am not sure of the best way to word this and have tried a few different searches with different combinations of words without success.
I only want to Sum Sequence = 1 when there are Sequence > 1, in the table below the Sequence = 1 lines marked with *. I don't care at all about checking that Sequence 2,3,etc match the same pattern because if they exist at all I need to Sum them.
I have data that looks like this:
| Sequence | ID | Num | OtherID |
|----------|----|-----|---------|
| 1 | 1 | 10 | 1 |*
| 2 | 1 | 15 | 1 |
| 3 | 1 | 20 | 1 |
| 1 | 2 | 10 | 1 |*
| 2 | 2 | 15 | 1 |
| 1 | 3 | 10 | 1 |
| 1 | 1 | 40 | 3 |
I need to sum the Num column but only when there is more than one sequence. My output would look like this:
Sequence Sum OtherID
1 20 1
2 30 1
3 20 1
I have tried grouping the queries in a bunch of different ways but really by the time I get to the sum, I don't know how to look ahead to make sure there are greater than 1 sequences for an ID.
My query at the moment looks something like this:
select Sequence, Sum(Num) as [Sum], OtherID
from tbl
where ID in (Select ID from tbl where Sequence > 1)
Group by Sequence, OtherID
tbl is a CTE that I wrapped around my query and it partially works, but is not really the filter I wanted.
If this is something that just shouldn't be done or can't be done then I can handle that, but if it's something I am missing I'd like to fix the query.
Edit:
I can't give the full query here but I started with this table/data (to get the above output). The OtherID is there because the data has the same ID/Sequence combinations but that OtherID helps separate them out so the rows are not identical (multiple questions on a form).
Create table #tmpTable (ID int, Sequence int, Num int, OtherID int)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (1, 1, 10, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (1, 2, 15, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (1, 3, 20, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (2, 1, 10, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (2, 2, 15, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (3, 1, 10, 1)
insert into #tmpTable (ID, Sequence, Num, OtherID) values (1, 1, 40, 3)
The following will sum over Sequence and OtherID, but only when:
Either
sequence is greater than 1
or
there is something else with the same ID and OtherID, but a different sequence.
Query:
select Sequence, Sum(Num) as SumNum, OtherID from #tmpTable a
where Sequence > 1
or exists (select * from #tmpTable b
where a.ID = b.ID
and a.OtherID = b.OtherID
and b.Sequence <> a.Sequence)
group by Sequence, OtherID;
It looks like you are trying to sum by Sequence and OtherID if the Count of ID >1, so you could do something like below:
select Sequence, Sum(Num) as [Sum], OtherID
from tbl
where ID in (Select ID from tbl where Sequence > 1)
Group by Sequence, OtherID
Having count(id)>1

Grouping Multiple Levels in a Query

So this is what I have.
Have a function that returns loan exceptions. So if someone has a missing document, that's an exception, or a signature required, that's an exception etc.
The problem is that ALL of the information being returned by this function contains all of the information for the loan. Including the amount.
So if there are 6 exceptions on a single loan, and the loan is 1000, then totaling the amount by exceptions gives you 6000, because 1000 is stored in every record detail.
So here is a similar set of records that I am returning.
poolDesc| loanNumber| Exception | Amount
Consumer| 123 | Missing Sig| 100
Consumer| 123 | Missing Doc| 100
Consumer| 123 | Late Pymt | 100
Estate | 456 | Address Ent| 2000
Estate | 456 | Missing Doc| 2000
Estate | 789 | Missing Sig| 1000
Consumer| 345 | Missing Sig| 500
What I am looking for out of that selection is:
POOL CountExceptions LoanAmount
Consumer 4 600
Estate 3 3000
There has to be a way to do this, and its going to an SSRS report if that helps.
Thanks
SELECT poolDesc Pool,
SUM(CountExceptions) CountExceptions,
SUM(LoanAmount) LoanAmount
FROM (
SELECT
poolDesc ,
COUNT(*) CountExceptions,
SUM(Amount) OVER (PARTITION BY poolDesc, loanNumber ) LoanAmount
FROM
loanExceptions
GROUP BY poolDesc, loanNumber, Amount
) a
GROUP BY poolDesc
full test script
create table loanExceptions (
poolDesc varchar(50),
loanNumber int,
Exception varchar(50),
Amount float)
insert into loanExceptions values
('Consumer',123,'Missing Sig',100),
('Consumer',123,'Missing Doc',100),
('Consumer',123,'Late Pymt',100),
('Estate',456,'Address Ent',2000),
('Estate',456,'Missing Doc',2000),
('Estate',789,'Missing Sig',1000),
('Consumer',345,'Missing Sig',500)
SELECT poolDesc Pool,
SUM(CountExceptions) CountExceptions,
SUM(LoanAmount) LoanAmount
FROM (
SELECT
poolDesc ,
COUNT(*) CountExceptions,
SUM(Amount) OVER (PARTITION BY poolDesc, loanNumber ) LoanAmount
FROM
loanExceptions
GROUP BY poolDesc, loanNumber, Amount
) a
GROUP BY poolDesc
DROP TABLE loanExceptions

How to insert master detail records in T-SQL?

I need to copy some master-detail records, along the lines of:
INSERT INTO Order
(
SupplierId
,DateOrdered
)
SELECT
SupplierID
,DateOrdered
FROM Order
WHERE SupplierId = 10
DECLARE #OrderId int;
Select #OrderId = Scope_Identity;
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
WHERE FkOrderId = #OrderId
This will not work. The reason is that there are multiple Orders for Supplier = 10. So what is the best way to iterate through each Order where Supplier = 10, Add the order, and then add the relevant child OrderItem BEFORE going onto the next Order Record where supplier=10. I think I am talking about batching, possibly cursors, but I am a newbie to T-SQL / Store Procedures.
I would appreciate advice on the above.
Thanks.
EDIT
Some more information which I hope will clarify by virtue of some sample data.
Original Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
Original OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
Final Order Table
Id SupplierId DateOrdered
1 10 01/01/2000
2 10 01/01/2000
3 10 01/01/2000 (Clone of 1)
4 10 01/01/2000 (Clone of 2)
Final OrderItem Table
Id Quantity ProductCode Price FkOrderId
1 20 X1 100 1
2 10 Y1 50 1
3 30 X1 100 2
4 20 Y1 50 2
5 20 X1 100 3 (Clone of 1, linked to clone Order=3)
6 10 Y1 50 3 (Clone of 2, linked to clone Order=3)
7 30 X1 100 4 (Clone of 3, linked to clone Order=4)
8 20 Y1 50 5 (Clone of 4, linked to clone Order=4)
So I need some help with the code can do this cloning of Order and OrderItem to achieve the "final" table records.
It seems I need to do something like:
For each matching record in "Order"
Clone Order Record
Clone OrderItem Record where FkOrderId = OldOrderId
Next
This answers your question (no cursors either)
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE [Order]
(
Id Int Primary Key Identity,
SupplierId Int,
DateOrdered Date
)
SET IDENTITY_INSERT [Order] ON
INSERT INTO [Order] (Id, SupplierId, DateOrdered)
VALUES
(1, 10, '01/01/2000'),
(2, 10, '01/01/2000')
SET IDENTITY_INSERT [Order] OFF
CREATE TABLE [OrderItem]
(
ID INT Primary Key Identity,
Quantity Int,
ProductCode CHAR(2),
Price Int,
FKOrderId Int
)
SET IDENTITY_INSERT [OrderItem] ON
INSERT INTO [OrderItem] (Id, Quantity, ProductCode, Price, FKOrderId)
VALUES
(1, 20, 'X1', 100, 1),
(2, 10, 'Y1', 50, 1),
(3, 30, 'X1', 100, 2),
(4, 20, 'Y1', 50, 2)
SET IDENTITY_INSERT [OrderItem] OFF
Query 1:
DECLARE #NewEntries TABLE (ID Int, OldId Int);
MERGE INTO [Order]
USING [Order] AS cf
ON 1 = 0 -- Ensure never match - therefore an Insert
WHEN NOT MATCHED AND cf.SupplierId = 10 THEN
INSERT(SupplierId, DateOrdered) Values(cf.SupplierId, cf.DateOrdered)
Output inserted.Id, cf.Id INTO
#NewEntries(Id, OldId);
INSERT INTO [OrderItem]
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,NE.ID
FROM [OrderItem] OI
INNER JOIN #NewEntries NE
ON OI.FKOrderId = NE.OldId ;
SELECT *
FROM [OrderItem];
Results:
| ID | QUANTITY | PRODUCTCODE | PRICE | FKORDERID |
|----|----------|-------------|-------|-----------|
| 1 | 20 | X1 | 100 | 1 |
| 2 | 10 | Y1 | 50 | 1 |
| 3 | 30 | X1 | 100 | 2 |
| 4 | 20 | Y1 | 50 | 2 |
| 5 | 20 | X1 | 100 | 3 |
| 6 | 10 | Y1 | 50 | 3 |
| 7 | 30 | X1 | 100 | 4 |
| 8 | 20 | Y1 | 50 | 4 |
Add an additional column to the Order table called OriginalOrderId. Make it nullable, FK'd back to OrderId, and put an index on it. Use "INSERT INTO [Order]... SELECT ... OUTPUT INSERTED.* INTO #ClonedOrders From ...". Add an index on #ClonedOrders.OriginalOrderId. Then you can do "INSERT INTO OrderItem ... SELECT co.OrderId, ... FROM #ClonedOrders co INNER JOIN OrderItem oi ON oi.OrderId = co.OriginalOrderId". This will get you the functionality that you're looking for, along with the performance benefits of set based statements. It will also leave you evidence of the original source of the orders and a field that you can use to differentiate cloned orders from non-cloned orders.
in this case you have to use output clause.. let me give you one sample script that will help you to relate with your requirement
Declare #Order AS Table(id int identity(1,1),SupplierID INT)
DECLARE #outputOrder AS TABLE
(Orderid INT)
INSERT INTO #Order (SupplierID)
Output inserted.id into #outputOrder
Values (102),(202),(303)
select * from #outputOrder
next step for your case would be use newly generated orderid from outputorder table & join to get orderitems from input table
This will handle your first table.
PS: Supply your questions in this state and they will be answered faster.
IF OBJECT_ID('Orders') IS NOT NULL DROP TABLE Orders
IF OBJECT_ID('OrderItem') IS NOT NULL DROP TABLE OrderItem
IF OBJECT_ID('tempdb..#FinalOrders') IS NOT NULL DROP TABLE #FinalOrders
CREATE TABLE Orders (OrdersID INT, SupplierID INT, DateOrdered DATETIME)
CREATE TABLE OrderItem (OrderItemID INT, Quantity INT, FkOrderId INT)
INSERT INTO Orders VALUES (1,20,'01/01/2000'),(2,20,'01/01/2000')
INSERT INTO OrderItem VALUES
(1,20,1),
(2,10,1),
(3,30,2),
(4,20,2)
SELECT
a.OrderItemID,
b.SupplierID,
b.DateOrdered
INTO #FinalOrders
FROM OrderItem as a
INNER JOIN Orders as b
ON a.FkOrderId = b.OrdersID
SELECT * FROM #FinalOrders
This can be achieved with a cursor. But please note that cursors will pose significant performance drawbacks.
DECLARE #SupplierID AS INT
DECLARE #OrderId AS INT
DECLARE #DateOrdered AS DATE
DECLARE #OrderIdNew AS INT
Declare #Order AS Table(OrderId INT,SupplierID INT,DateOrdered Date)
INSERT INTO #Order
SELECT
ID
,SupplierID
,DateOrdered
FROM [Order]
WHERE SupplierId = 10
DECLARE CUR CURSOR FAST_FORWARD FOR
SELECT
OrderId
,SupplierID
,DateOrdered
FROM #Order
OPEN CUR
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [Order]
(
SupplierId
,DateOrdered
)
VALUES
(#SupplierID,#DateOrdered)
Select #OrderIdNew=##IDENTITY
INSERT INTO [OrderItem]
([Quantity]
,[ProductCode]
,[Price]
,[FkOrderId])
SELECT [Quantity]
,[ProductCode]
,[Price]
,#OrderIdNew
FROM [OrderItem]
WHERE [FkOrderId]=#OrderId
FETCH NEXT FROM CUR INTO #OrderId, #SupplierID, #DateOrdered
END
CLOSE CUR;
DEALLOCATE CUR;
You could try doing and inner join between Order and OrderItems where the clause of the inner join is SupplierId = 10,
or just modify your where to achieve the same result.
Try doing something along the lines of:
INSERT INTO OrderItem
(
Quantity
,ProductCode
,Price
,FkOrderId
)
SELECT
Quantity
,ProductCode
,Price
,FkOrderId
FROM OrderItem
where FkOrderId in (Select Id FROM Order WHERE SupplierId = 10)

Resources