Masking ID and Name in SQL - sql

I have a use case where there is a free text field and the user id in the format ab12345 (fixed) and name (dynamic) can appear anywhere in the string.
Now I need to replace the ab12345 with xxxxxxx and the names also with XXXX wherever I find them in the string.
I used:
select *
from dbo.TEST
WHERE DESCRIPTION like '%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%';
to get the user id ab12345 but I am unable to write the replace function for this since the result is dynamic.
Same with the name as well.

the following may help in redacting the userID
USE tempdb
GO
CREATE TABLE #CustComments
( CustomerID INT
, CustomerNotes VARCHAR(8000)
)
GO
INSERT dbo.#CustComments
( CustomerID
, CustomerNotes
)
VALUES
( 1, 'An infraction was raised on user id ab12345, and the name of the complainant is John')
, ( 2, 'The customer was not happy with person CD45678 and is going to ask William Jones to speak with George Hillman about this matter' )
, ( 3, 'A customer called and repeatedly mentioned the name of employee ZX98765 and assumes their name was Janet which is not correct')
SELECT * ,
PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes) start_pos,
SUBSTRING (customernotes, (PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes)) ,7 ) extractstring,
REPLACE(customernotes, substring (customernotes, (PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes)) ,7 ), 'XXXXXXX') redacted
FROM #CustComments
--TIDY UP
DROP TABLE #CustComments

if you have or can create a table of "names"...this may work
USE tempdb
GO
CREATE TABLE #CustComments (
CustomerID int,
CustomerNotes varchar(8000)
)
GO
INSERT #CustComments (CustomerID
, CustomerNotes)
VALUES (1, 'An infraction was raised on user id ab12345 , and the name of the complainant is Ann')
, (2, 'The customer was not happy with person CD45678 and is going to ask Richard Jones to speak with Todd Hillman about this matter')
, (3, 'A customer called and repeatedly mentioned the name of employee ZX98765 and assumes their name was Shana which is not correct')
CREATE TABLE #empname (
ename varchar(255) NOT NULL
)
GO
INSERT INTO #empname ([ename])
VALUES ('Zeph'), ('Ebony'), ('Felicia'), ('Benedict'), ('Ahmed'), ('Ira'), ('Julie'), ('Levi'),
('Sebastian'), ('Fiona'), ('Lamar'), ('Russell'), ('Abdul'), ('Lev'), ('Isaiah'), ('Charlotte'),
('Rowan'), ('Ivory'), ('Quinn'), ('Jordan'), ('Xantha'), ('Shana'), ('Mufutau'), ('Jessamine'),
('Desirae'), ('Yvette'), ('Odessa'), ('Ray'), ('Ori'), ('Zenaida'), ('Allegra'), ('Allistair'),
('Raymond'), ('Martena'), ('Cameron'), ('Ila'), ('Nigel'), ('Dale'), ('Emerald'), ('Guinevere'),
('Boris'), ('Dolan'), ('Ainsley'), ('Madeson'), ('Kadeem'), ('Ciaran'), ('Hop'), ('Louis'),
('Maia'), ('Hiroko'), ('Hakeem'), ('Cole'), ('Tyrone'), ('Amy'), ('Doris'), ('Keaton'),
('Carlos'), ('Richard'), ('Lysandra'), ('Beverly'), ('Hamish'), ('Demetria'), ('Eric'), ('Nayda'),
('Sydney'), ('Fritz'), ('Blaze'), ('Regina'), ('Ciara'), ('Ina'), ('Joan'), ('Risa'),
('Alea'), ('Denton'), ('Daryl'), ('Mollie'), ('Keane'), ('Jarrod'), ('Ann'), ('Juliet'),
('Germaine'), ('Alexa'), ('Zane'), ('Kiona'), ('Armand'), ('Jin'), ('Geraldine'), ('Natalie'),
('Nomlanga'), ('Todd'), ('Rajah'),('Lucian'), ('Idona'), ('Autumn'), ('Briar'),
-- add surname
('Hillman');
;
-- redact the userID std format
SELECT
CustomerID ,
--PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes) start_pos,
--SUBSTRING (customernotes, (PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes)) ,7 ) extractstring,
REPLACE(customernotes, substring (customernotes, (PATINDEX('%[a-zA-z][a-zA-Z][0-9][0-9][0-9][0-9][0-9]%', CustomerNotes)) ,7 ), 'XXXXXXX') ID_redacted
INTO #ID_REDACT
FROM #CustComments
-- split into rows
SELECT customerId, value
into #SPLIT
FROM #ID_REDACT
CROSS APPLY STRING_SPLIT(ID_redacted, ' ');
--redact based on join with a ""name"" table
SELECT s.customerid,
CASE
WHEN e.ename IS NULL THEN s.value
ELSE 'XXXXXXX'
END AS name_redact
INTO #NAME_REDACT
FROM #split AS s
LEFT OUTER JOIN #empname AS e
ON s.value = e.ename
SELECT customerId,
STRING_AGG(name_redact, ' ') as full_redact
INTO #RESULTS
from #NAME_REDACT
group by CustomerID
-- RESULTS WITH COMPARISON
SELECT
C.CustomerID,
C.CustomerNotes AS Original,
R.full_redact AS Redacted
FROM #CustComments AS C
INNER JOIN #RESULTS AS R
ON C.CustomerID = R.customerId
--TIDY UP
DROP TABLE #CustComments
DROP TABLE #empname
DROP TABLE #ID_REDACT
DROP TABLE #SPLIT
DROP TABLE #NAME_REDACT
DROP TABLE #RESULTS

Related

SQL Server stored procedure looping through a comma delimited cell

I am trying to figure out how to go about getting the values of a comma separated string that's present in one of my cells.
This is the query I current am trying to figure out in my stored procedure:
SELECT
uT.id,
uT.permissions
FROM
usersTbl AS uT
INNER JOIN
usersPermissions AS uP
/*Need to loop here I think?*/
WHERE
uT.active = 'true'
AND
uT.email = 'bbarker#thepriceisright.com'
The usersPermissions table looks like this:
And so a row in the usersTbl table looks like this for permissions:
1,3
I need to find a way to loop through that cell and get each number and place the name ****, in my returned results for the usersTbl.permissions.
So instead of returning this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | 1,3 | 87 |
It needs to returns this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | Read,Upload | 87 |
Really just replacing 1,3 with Read,Upload.
Any help would be great from a SQL GURU!
Reworked query
SELECT
*
FROM
usersTbl AS uT
INNER JOIN
usersPermissionsTbl AS uPT
ON
uPT.userId = uT.id
INNER JOIN
usersPermissions AS uP
ON
uPT.permissionId = uP.id
WHERE
uT.active='true'
AND
uT.email='bBarker#thepriceisright.com'
I agree with all of the comments... but strictly trying to do what you want, here's a way with a splitter function
declare #usersTbl table ([Name] varchar(64), id int, [permissions] varchar(64), age int)
insert into #usersTbl
values
('Bbarker',5987,'1,3',87)
declare #usersTblpermissions table (id int, [type] varchar(64))
insert into #usersTblpermissions
values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
;with cte as(
select
u.[Name]
,u.id as UID
,p.id
,p.type
,u.age
from #usersTbl u
cross apply dbo.DelimitedSplit8K([permissions],',') x
inner join #usersTblpermissions p on p.id = x.Item)
select distinct
[Name]
,UID
,age
,STUFF((
SELECT ',' + t2.type
FROM cte t2
WHERE t.UID = t2.UID
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from cte t
Jeff Moden Splitter
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
First, you should read Is storing a delimited list in a database column really that bad?, where you will see a lot of reasons why the answer to this question is Absolutely yes!
Second, you should add a table for user permissions since this is clearly a many to many relationship.
Your tables might look something like this (pseudo code):
usersTbl
(
Id int primary key
-- other user related columns
)
usersPermissionsTbl
(
UserId int, -- Foreign key to usersTbl
PermissionId int, -- Foreign key to permissionsTbl
Primary key (UserId, PermissionId)
)
permissionsTbl
(
Id int primary key,
Name varchar(20)
)
Once you have your tables correct, it's quite easy to get a list of comma separated values from the permissions table.
Adapting scsimon's sample data script to a correct many to many relationship:
declare #users table ([Name] varchar(64), id int, age int)
insert into #users values
('Bbarker',5987,87)
declare #permissions table (id int, [type] varchar(64))
insert into #permissions values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
declare #usersPermissions as table (userId int, permissionId int)
insert into #usersPermissions values (5987, 1), (5987, 3)
Now the query looks like this:
SELECT u.Name,
u.Id,
STUFF(
(
SELECT ','+ [type]
FROM #permissions p
INNER JOIN #usersPermissions up ON p.id = up.permissionId
WHERE up.userId = u.Id
FOR XML PATH('')
)
, 1, 1, '') As Permissions,
u.Age
FROM #Users As u
And the results:
Name Id Permissions Age
Bbarker 5987 Read,Upload 87
You can see a live demo on rextester.
I concur with much of the advice being presented to you in the other responses. The structure you're starting with is not going to be fun to maintain and work with. However, your situation may mean you are stuck with it so maybe some of the tools below will help you.
You can parse the delimiter with charindex() as others demonstrated here- MSSQL - How to split a string using a comma as a separator
... and even better here (several functions are provided) - Split function equivalent in T-SQL?
If you still want to do it with raw inline SQL and are committed to a loop, then pair the string manipulation with a CURSOR. Cursors have their own controversies BTW. The code below will work if your permission syntax remains consistent, which it probably doesn't.
They used charindex(',',columnName) and fed the location into the left() and right() functions along with some additional string evaluation to pull values out. You should be able to piece those together with a cursor
Your query might look like this...
--creating my temp structure
declare #userPermissions table (id int, [type] varchar(16))
insert into #userPermissions (id, [type]) values (1, 'Read')
insert into #userPermissions (id, [type]) values (2, 'Write')
insert into #userPermissions (id, [type]) values (3, 'Upload')
insert into #userPermissions (id, [type]) values (4, 'Admin')
declare #usersTbl table ([Name] varchar(16), id int, [permissions] varchar(8), age int)
insert into #usersTbl ([Name], id, [permissions], age) values ('Bbarker', 5987, '1,3', 87)
insert into #usersTbl ([Name], id, [permissions], age) values ('Mmouse', 5988, '2,4', 88)
--example query
select
ut.[Name]
, (select [type] from #userPermissions where [id] = left(ut.[permissions], charindex(',', ut.[permissions])-1) )
+ ','
+ (select [type] from #userPermissions where [id] = right(ut.[permissions], len(ut.[permissions])-charindex(',', ut.[permissions])) )
from #usersTbl ut

select results from two tables but choose one field over another

I have two tables.
tblEmployee tblExtraOrMissingInfo
id nvarchar(10) id nvarchar(10)
Name nvarchar(50) Name nvarchar(50)
PreferredName nvarchar(50)
UsePreferredName bit
The data (brief example)
tblEmployee tblExtraOrMissingInfo
id Name id Name PreferredName UsePreferredName
AB12 John PN01 Peter Tom 1
LM22 Lisa YH76 Andrew Andy 0
PN01 Peter LM22 Lisa Liz 0
LK655 Sarah
I want a query to produce the following result
id Name
AB12 John
LM22 Lisa
PN01 Tom
YH76 Andrew
LK655 Sarah
So what I want is all the records from tblEmployee returned and any records in tblExtraOrMissingInfo that are not already in tblEmployee.
If there is a record in both tables with the same id I would like is if the UsePreferredName field in tblExtraOrMissingInfo is 1 for the PreferredName to be used rather than the Name field in the tblEmployee, please see the record PN01 in the example above.
It is slightly faster to use a left join and coalesce than to use the case statement (most servers are optimized for coalesce).
Like this:
SELECT E.ID, COALESCE(P.PreferredName,E.Name,'Unknown') as Name
FROM tblemployee E
LEFT JOIN tblExtraOrMissingInfo P ON E.ID = P.ID AND P.UsePreferredName = 1
The ,'Unknown' is not needed to answer your question, but I added
here to show that you can enhance this query to handle cases where the
name is not available in both tables and you don't want nulls in your result
left join on the employee table and use a case expression for name.
select e.id
,case when i.UsePreferredName = 1 then i.PreferredName else e.name end as name
from tblemployee e
left join tblExtraOrMissingInfo i on i.id=e.id
You can LEFT OUTER JOIN the tables together, then use a CASE statement and COALESCE() formula to get this:
SELECT
tblEmployee.id,
CASE WHEN UsePreferredName = 1 THEN COALESCE(PreferredName, tblEmployee.Name) ELSE tblEmployee.name
FROM
tblEmployee
LEFT OUTER JOIN tblExtraOrMissingInfo
ON tblEmployee.id = tblExtraOrMissingInfo.id
CASE checks that usePreferredName value, and the COALESCE() then grabs PreferredName unless it's NULL. Then it grabs the employees name from tblEmployee.
select id ,name
from
(
select id ,name from tblEmployee
union all
select id ,name from tblExtraOrMissingInfo
)group by id,name
This is an other way you can achieve the results. You can also use CTE to get the same result.
CREATE TABLE #tblEmployee
(
id NVARCHAR(10)
, Name NVARCHAR(50)
);
CREATE TABLE #tblExtraOrMissingInfo
(
id NVARCHAR(10)
, Name NVARCHAR(50)
, PreferredName NVARCHAR(50)
, UsePreferredName BIT
);
INSERT INTO #tblEmployee
( id, Name )
VALUES ( N'AB12' -- id - nvarchar(10)
, N'John' -- Name - nvarchar(50)
),
( N'LM22' -- id - nvarchar(10)
, N'Lisa' -- Name - nvarchar(50)
),
( N'PN01' -- id - nvarchar(10)
, N'Peter' -- Name - nvarchar(50)
),
( N'LK655' -- id - nvarchar(10)
, N'Sarah' -- Name - nvarchar(50)
);
INSERT INTO #tblExtraOrMissingInfo
( id, Name, PreferredName, UsePreferredName )
VALUES ( N'PN01' -- id - nvarchar(10)
, N'Peter' -- Name - nvarchar(50)
, N'Tom' -- PreferredName - nvarchar(50)
, 1 -- UsePreferredName - bit
),
( N'YH76' -- id - nvarchar(10)
, N'Andrew' -- Name - nvarchar(50)
, N'Andy' -- PreferredName - nvarchar(50)
, 0 -- UsePreferredName - bit
),
( N'LM22' -- id - nvarchar(10)
, N'Lisa' -- Name - nvarchar(50)
, N'Liz' -- PreferredName - nvarchar(50)
, 0 -- UsePreferredName - bit
);
SELECT R.id
, CASE WHEN tbl.UsePreferredName = 1 THEN tbl.PreferredName
ELSE R.Name
END AS NAME
FROM #tblExtraOrMissingInfo tbl
RIGHT JOIN ( SELECT id
, Name
FROM #tblEmployee
UNION
SELECT id
, Name
FROM #tblExtraOrMissingInfo
) AS R
ON R.id = tbl.id
AND R.Name = tbl.Name;

Query to Append a Number to a Record it it finds a duplicate

I have an sql table with the following fields: Letter, Number, Result
Title Name Result
Mr Mark
Mr Mark
Mr Luke
Mr John
Mr John
I need to create an update query to have the result as
Title Name Result
Mr Mark MrMark
Mr Mark MrMark2
Mr Luke MrLuke
Mr John MrJohn
Mr John MrJohn2
Note that the second and the fifth record had a number 2 appended since it already found the same record (same Title and Name) previously.
Please help.
If it is MS SQL, try using ROW_NUMBER and PARTITION BY?
DECLARE #temp TABLE (Title NVARCHAR(200), Name NVARCHAR(200), Result NVARCHAR(200));
INSERT #temp
SELECT 'Mr', 'Mark', NULL
UNION ALL
SELECT 'Mr', 'Mark', NULL
UNION ALL
SELECT 'Mr', 'Luke', NULL
UNION ALL
SELECT 'Mr', 'John', NULL
UNION ALL
SELECT 'Mr', 'John', NULL
SELECT * FROM #temp
DECLARE #tempWithOrdering TABLE (RowNum INT, Title NVARCHAR(200), Name NVARCHAR(200), Result NVARCHAR(200));
INSERT #tempWithOrdering
SELECT ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Title ), Title, Name, Result FROM #temp
SELECT * FROM #tempWithOrdering
SELECT
Title,
Name,
Result = (
SELECT TOP(1) Name +
CASE RowNum
WHEN t1.RowNum THEN ''
ELSE CAST(t1.RowNum AS NVARCHAR(12))
END
FROM #tempWithOrdering
WHERE Name = t1.Name
)
FROM #tempWithOrdering t1
Assuming you are using sql server and the duplicate is on the field 'Name' .Try this.
Use Analytic fn ROW_NUMBER() ;
If it is Oracle use || instead of +
WITH TEMP AS
(
SELECT Title , Name,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY bill_period) AS RK
FROM TABLE1
)
SELECT Title , Name,Title + Name +RK FROM TEMP;

Split string, copy data into multiple tables

I have the following tables
Table A
RID | Name |Phone |Email |CreatedOn
------------------------------------------------------------
1 | John Smith | 2143556789 |t1#gmail.com |2012-09-01 09:30:00
2 | Jason K Crull | 2347896543 |t2#gmail.com |2012-08-02 10:34:00
Table B
CID| FirstName |LastName |Phone |Email |CreatedOn |Title|Address|City|State
---------------------------------------------------------------------------------------------------
11 | John | Smith |2143556789 |t1#gmail.com |2012-09-01 09:30:00|NULL|NULL|NULL|NULL
12 | Jason | K Crull |2347896543 |t2#gmail.com |2012-08-02 10:34:00|NULL|NULL|NULL|NULL
Table C
RID | CID |IsAuthor|CreatedOn
-----------------------------------------
1 | 11 | 0 |2012-09-01 09:30:00
2 | 12 | 0 |2012-08-02 10:34:00
For every row in "Table A" need to create a row in "Table B" splitting the name into First and Last Name as shown and after creating a row, insert new row into Table C with RID from Table A, CID from Table B, IsAuthor bit Default to 0 and CreatedOn from Table A.The CID is auto incremented. Can anyone help me in achieving this. I am very new to SQL. Thanks!
I believe you're looking for something like this (I left off some fields, but this should get the point across). Main thing to see is the substring and charindex functions which are used to split the name into first name and last names:
insert into tableb (firstname,lastname,phone,email)
select
left(name, charindex(' ',name)-1),
substring(name, charindex(' ', name)+1, len(name)),
phone, email
from tablea ;
insert into tablec
select a.rid, b.cid, 0, a.createdon
from tablea a
inner join tableb b on a.name = b.firstname + ' ' + b.lastname
and a.phone = b.phone and a.email = b.email ;
SQL Fiddle Demo
If there is a concern for the same names, emails, etc, then you're probably going to need to look into using a dreaded cursor and scope_identity(). Hopefully you won't have to go down that route.
Stop thinking about "for every row" and think of it as "a set." Very rarely is it efficient to process anything row-by-row in SQL Server, and very rarely is it beneficial to think in those terms.
--INSERT dbo.TableC(RID, CID, IsAuthor, CreatedOn)
SELECT a.RID, b.CID, IsAuthor = 0, a.CreatedOn
FROM dbo.TableA AS a
INNER JOIN dbo.TableB AS b
ON a.Name = b.FirstName + ' ' b.LastName;
When you believe it is returning the right results, uncomment the INSERT.
To split the name I'd use CharIndex to find the position of the space, then Substring to break the word apart.
For keeping track of which row in TableA the data in TableB came from, I'd just stick a column onto B to record this data, then drop it when you come to inset into table C.
An alternative would be to make CID an identity column on C instead of B, populate C first, then feed that data into TableB when you come to populate that.
if OBJECT_ID('TableA','U') is not null drop table TableA
create table TableA
(
rid int not null identity(1,1) primary key clustered
, Name nvarchar(64)
, Phone nvarchar(16)
, Email nvarchar(256)
, CreatedOn datetime default (getutcdate())
)
if OBJECT_ID('TableB','U') is not null drop table TableB
create table TableB
(
cid int not null identity(1,1) primary key clustered
, FirstName nvarchar(64)
, LastName nvarchar(64)
, Phone nvarchar(16)
, Email nvarchar(256)
, CreatedOn datetime default (getutcdate())
, Title nvarchar(16)
, [Address] nvarchar(256)
, City nvarchar(64)
, [State] nvarchar(64)
)
if OBJECT_ID('TableC','U') is not null drop table TableC
create table TableC
(
rid int primary key clustered
, cid int unique
, IsAuthor bit default(0)
, CreatedOn datetime default (getutcdate())
)
insert TableA (Name, Phone, Email) select 'John Smith', '2143556789', 't1#gmail.com'
insert TableA (Name, Phone, Email) select 'Jason K Crull', '2347896543', 't2#gmail.com'
alter table TableB
add TempRid int
insert TableB(FirstName, LastName, Phone, Email, TempRid)
select case when CHARINDEX(' ', Name) > 0 then SUBSTRING(Name, 1, CHARINDEX(' ', Name)-1) else Name end
, case when CHARINDEX(' ', Name) > 0 then SUBSTRING(Name, CHARINDEX(' ', Name)+1, LEN(Name)) else '' end
, Phone
, Email
, Rid
from TableA
insert TableC (rid, cid)
select TempRid, cid
from TableB
alter table TableB
drop column TempRid
select * from TableB
select * from TableC
Try it here: http://sqlfiddle.com/#!3/aaaed/1
Or the alternate method (inserting to C before B) here: http://sqlfiddle.com/#!3/99592/1

Subquery: how to retrieve the last non-zero value from a column?

Considering a table customerBalance with 3 columns: name, date and balance. Suppose a set of records like:
cus_name cus_date cus_balance
John 06/14/2011 1000
John 06/15/2011 500
John 06/16/2011 0
Mary 06/14/2011 3000
Mary 06/15/2011 2800
Mary 06/16/2011 0
How to create a SQL query which returns, for the date 6/16/2011 instead 0, the last non-zero value based on date (in sample, $500 for John and $2800 for Mary)?
I'm trying to do it using a subquery which uses Max function to retrieve the last date with non-zero value, but I didn't succeed. This example is quite "nonsensical", but I really need to do an operation like this in my dataset. Thanks!
Note: If you can specify the DB and version this query can be improved.
Try this:
SELECT *
FROM customers
WHERE (cus_name, cus_date)
IN
(
SELECT cus_name, MAX(cus_date)
FROM customers
WHERE cus_balance <> 0
GROUP BY cus_name
)
Update: Alternate version:
SELECT a.*
FROM customers a,
(
SELECT cus_name, MAX(cus_date)
FROM customers
WHERE cus_balance <> 0
GROUP BY cus_name
) b
WHERE a.cus_name = b.cus_name
AND a.cus_date = b.cus_date
Here it goes:
CREATE Table #temp
(
Cus_Name VARCHAR(200) NULL,
Cus_Date Char(8) NULL,
Cus_Balance INT NULL
)
INSERT INTO #temp VALUES ('John' , '20110614' ,1000 )
INSERT INTO #temp VALUES ('John' , '20110615' , 500 )
INSERT INTO #temp VALUES ('John' , '20110616' , 0 )
INSERT INTO #temp VALUES ('Mary' , '20110614' ,3000 )
INSERT INTO #temp VALUES ('Mary' , '20110615' ,2800 )
INSERT INTO #temp VALUES ('Mary' , '20110616' , 0 )
SELECT
T.Cus_Name ,
MIN(t.Cus_Balance)
FROM #temp t
WHERE t.Cus_Balance <>0
GROUP BY t.Cus_Name
DROP TABLE #temp