Friday, December 9, 2016

Running a SQL Stored Procedure from Excel with Dynamic Parameters (and some common errors)

Step 1: Data tab – > From Other Sources -> From SQL Server

.
Step 2: Enter Credentials.

.
Step 3: Choose any small table or view. It doesn’t matter which one because we’ll be changing the connection anyway.

.
Step 4: In the Data Connection Wizard dialog, give your connection a friendly name that you can remember. This is how you’ll point to this connection in VBA. In this case, I’m calling this connection MYSERVER.

.
Step 5: Excel will pop up the Import Data dialog box – Click Properties here (NOT THE OK BUTTON).

.
Step 6: Click on the Definition tab. Here, you’ll want to change Command Type to SQL, and then enter your Stored Procedure name in the Command Text input. As you can see, the SQL statement is simply a call to the Stored Procedure along with the Parameter name (in this case, the procedure is expecting one parameter that accepts a market name).


.
Step 7: Excel warns you about something unimportant – Click Yes (which in this case, means go away).

.
Step 8: Excel will activate the Import Data dialog box again. This time click OK to fire the Stored Procedure and return the results.

.
Step 9: Note the results you get from your original hard-coded command you entered in Step 6. At this point, you know your connection is working.

.
Step 10: Create a mechanism to dynamically select and enter your parameters.
In my case, I need to pass a market name to my Stored Procedure. So I added a simple dropdown where my selection goes to Cell B2. The mechanism you choose to select your parameters is not the important thing here. The important thing is to note where the final selection will be housed. In this example, my market selection will end up in Cell B2.

.
Step 11: The final step is to copy and paste this Macro into a standard module. Notice that all we are dong here is changing the definition fo the CommandText property of the Connection. This basically means that as long as you can point to the correct connection (MYSERVER in this case), you can rebuild the CommandText on the fly. Here, I’m rebuilding the CommandText to point to my chosen cell range (cell B2 – where my market selection is housed).
  1. Sub RefreshQuery()
  2.     With ActiveWorkbook.Connections("MYSERVER").OLEDBConnection
  3.         .CommandText = "EXECUTE dbo.Tng_Market_Feed '" & Range("B2").Value &"'"
  4.     End With
  5.     ActiveWorkbook.Connections("MYSERVER").Refresh
  6.    
  7. End Sub
.
At this point, it’s just a question of firing the Macro each time your parameter selection changes.
Now you’re cooking with gas!


Error 1:
The query did not run, or the database table could not be opened. Check the database server or contact your DBA. Make sure the external database is available and hasn't been moved or recognized, then try the operation again
Solution: By wrapping your Stored Procedures with SET NOCOUNT ON … SET NOCOUNT OFF, you can fire all kinds of Stored Procedures from Excel:
Error 2:
Modify an embedded Connection String in microsoft excel macro
Solution: he connection string needs to have OLEDB; prepended to the string.
so this now works!!!
ActiveWorkbook.Connections("Transaction_Summary").OLEDBConnection.Connection = "OLEDB;Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=True;Initial Catalog=ADCData_Doric;Data Source=doric-server5;Use Procedure for Prepare=1;Auto Translate=True;Packet Size=4096;Workstation ID=LHOLDER-VM;Use Encryption for Data=False;Tag with column collation when possible=False"

Monday, November 7, 2016

Understanding ADO Transactions with MS SQL Server

SUMMARY
The ActiveX Data Objects (ADO) Connection method BeginTrans states that it begins a new Transaction, but it executes "set implicit_transactions on." In many cases, "set implicit_transactions on" is equivalent to T-SQL BEGIN TRANS. However, there are subtle differences. This article illustrates one of those differences.
MORE INFORMATION

How Does ADO Behave with Respect to Transactions

By default ADO operates in AutoCommit mode, unless you start a implicit transaction by executing Connection.BeginTrans. 

Implicit_transactions begin a transaction on the server for each statement, and commits do not occur until they are manually issued. 

So,

   set implicit_transactions on
   go
   insert
   insert
   insert
    
is internally turned into

   BEGIN TRAN
   Insert
   insert
   insert
   ...
    
The above transaction will not be rolled back or committed unless the user issues the correct statement. 

Without implicit transaction, which by default is the behavior of ADO (Auto Commit mode), the following is (conceptually) occurring:

   begin tran
   insert
   commit tran
   begin tran
   insert
   commit tran
   ...
    
The following code sequence, written in Visual Basic, shows a difference between the raw SQL "BEGIN TRANSACTION" and the "set implicit_transactions on" issued when the ADO connection method BeginTrans is invoked:
  1. Create a new standard .exe Visual Basic project. Add two command buttons to Form1, the default form.
    
          Command1.Caption : Use ADO Transactions
          Command2.Caption : Use T-SQL Transactions
          
  2. Go to the references and add the following reference:
    
          Microsoft ActiveX Data Objects 2.0 Library
          
  3. The code below uses the stores table of the Pubs database. Make sure you set the database properly in your connectstring.
  4. Paste the following lines into the Code window for Form1: 

    Note You must change the User ID value and the password = value to the correct values before you run this code. Make sure that User ID has the appropriate permissions to perform this operation on the database.
    
          Option Explicit
          Dim Cn As New ADODB.Connection
          Dim Cmd As New ADODB.Command
          Dim rst As New ADODB.Recordset
          Private Sub Command1_Click()
            Cn.Execute "Delete from stores where stor_id LIKE  '1%'"
            Cn.BeginTrans
            Cn.Execute "set implicit_transactions off"
            Cn.Execute "Insert INTO Stores(stor_id, _
            stor_name,stor_address,city)" & _
            "VALUES(101,'Store One','123 Oak St.','Seattle')"
            Cn.Execute "Insert INTO Stores(stor_id, _
            stor_name,stor_address,city)" & _
            "VALUES(102,'Store Two','123 Main St.','Tacoma')"
            Cn.RollbackTrans
            With rst
              .ActiveConnection = Cn
              .CursorType = adOpenStatic
              .Source = "select * from stores where stor_id LIKE '10%'"
              .Open
            End With
            MsgBox rst.RecordCount
            rst.Close
          End Sub
    
          Private Sub Command2_Click()
           Cn.Execute "Delete from stores where stor_id LIKE  '1%'"
           Cn.Execute "BEGIN TRANSACTION"
           Cn.Execute "set implicit_transactions off"
           Cn.Execute "Insert INTO Stores (stor_id, _
           stor_name,stor_address,city)" & _
           "VALUES(101,'Store One','123 Oak St.','Seattle')"
           Cn.Execute "Insert INTO Stores (stor_id, _
           stor_name,stor_address,city)" & _
           "VALUES(102,'Store Two','123 Main St.','Tacoma')"
           Cn.Execute "ROLLBACK TRANSACTION"
           With rst
              .ActiveConnection = Cn
              .CursorType = adOpenStatic
              .Source = "select * from stores where stor_id LIKE '10%'"
              .Open
            End With
           MsgBox rst.RecordCount
           rst.Close
          End Sub
    
          Private Sub Form_Load()
             Dim strConn As String
             strConn = "Provider=SQLOLEDB;User ID=;Password=;Data" & _
             "Source=(local);database=pubs"
             Cn.Open strConn
             Cn.CursorLocation = adUseClient
             Command1.Caption = "Use ADO Transactions"
             Command2.Caption = "Use T-SQL Transactions"
          End Sub
          
  5. Click on Command1. You will get a RecordCount of 2.
  6. Click on Command2. You will get a RecordCount of 0.

Reason for the Difference in Results

With ADO Connection.BeginTrans will not increment @@TranCount until any of the following statements are encountered:
   FETCH      ALTER TABLE
   DELETE     INSERT
   CREATE     OPEN
   GRANT      REVOKE
   DROP       TRUNCATE TABLE
   SELECT     UPDATE
    
When this option (set implicit_transactions on) is turned on and if there are no outstanding transactions, every ANSI SQL statement will automatically start a transaction. If there is an open transaction, no new transaction will be started. This transaction has to be explicitly committed by the user by using the command COMMIT TRANSACTION for the changes to take affect and the locks to be released. 

Whereas executing Cn.Execute "BEGIN TRANSACTION" starts an explicit transaction and @@TranCount is immediately incremented by 1. 

In the above Visual Basic code for the procedure Command1_Click() the @@TranCount remains at 0 and the Rollback has no effect because before executing the first insert, you revert back to Auto Commit mode by issuing Cn.Execute "set implicit_transactions off." 

Whereas in the Visual Basic code for procedure Command2_Click(), the @@TranCount is immediately incremented to 1 when you issue Cn.Execute "BEGIN TRANSACTION; hence, the subsequent RollBack works. 

The SQL Server 6.5 Trace utility is very useful for observing the Transaction commands this sample issues.
REFERENCES
For additional information, please see the following article(s) in the Microsoft Knowledge Base:
177138 INFO: Nested Transactions Not Available in ODBC/OLE DB/ADO
Properties
Article ID: 198024 - Last Review: 10/31/2003 06:41:17 - Revision: 2.0
Microsoft OLE DB Provider for SQL Server 7.0, Microsoft ActiveX Data Objects 1.5, Microsoft ActiveX Data Objects 2.0, Microsoft ActiveX Data Objects 2.1, Microsoft ActiveX Data Objects 2.5, Microsoft ActiveX Data Objects 2.6, Microsoft ODBC Driver for Microsoft SQL Server 3.7
  • kbcode kbdatabase kbinfo KB198024

Friday, November 4, 2016

Execute SQL stored procedure from Access 2010


  1. Create pass-through query in Access with any line of code, it doesn't matter because we will change this code before execution. call this query for example "PTQ"
  2.  Use the following VBA code, pay attention to a few details
    • ReturnsRecords must be set to False if the stored procedure dosen't return any records
    • Query "PTQ" must be a pass-through query, with a Connect property set properly


    Dim qdf As DAO.QueryDef
    Set qdf = CurrentDb.QueryDefs("PTQ")
    qdf.SQL = "exec myStoredProcedure '" & parameter & "'"
    qdf.ReturnsRecords = False
    qdf.Execute dbFailOnError

Monday, September 26, 2016

Excel VBA exception handling & performance boost

Sub MainMacro()
    Dim calculationMode As Integer
    calculationMode = Application.Calculation
   
    On Error GoTo Err_Finally
    Application.Calculation = xlCalculationManual
    Application.ScreenUpdating = False
   
    Do Something......
   
Exit_Sub:
    Application.CutCopyMode = False
   
    Application.ScreenUpdating = True
    Application.Calculation = calculationMode
    Exit Sub
   
Err_Finally:
    MsgBox Err.Description
    Resume Exit_Sub
End Sub

Friday, September 23, 2016

Document Access Database (*.mdb)

Public Sub DocumentDatabase()
    Dim path As String
    path = Application.CurrentProject.path & "\" + Replace(Application.CurrentProject.Name, ".mdb", "") + "\"
   
    On Error Resume Next
    Kill path & "Forms\*.*"
    Kill path & "Reports\*.*"
    Kill path & "Macros\*.*"
    Kill path & "Modules\*.*"
    Kill path & "Queries\*.*"
   
    On Error GoTo Err_DocDatabase
    Dim dbs As Database
    Dim cnt As Container
    Dim doc As Document
    Dim i As Integer
   
    Set dbs = CurrentDb() ' use CurrentDb() to refresh Collections
   
    Set cnt = dbs.Containers("Forms")
    For Each doc In cnt.Documents
        Application.SaveAsText acForm, doc.Name, path & "Forms\" & doc.Name & ".frm"
    Next doc
   
    Set cnt = dbs.Containers("Reports")
    For Each doc In cnt.Documents
        Application.SaveAsText acReport, doc.Name, path & "Reports\" & doc.Name & ".rpt"
    Next doc
   
    Set cnt = dbs.Containers("Scripts")
    For Each doc In cnt.Documents
        Application.SaveAsText acMacro, doc.Name, path & "Macros\" & doc.Name & ".vbs"
    Next doc
   
    Set cnt = dbs.Containers("Modules")
    For Each doc In cnt.Documents
        Application.SaveAsText acModule, doc.Name, path & "Modules\" & doc.Name & ".bas"
    Next doc
   
    'Dim obj As Object
    'For Each obj In Application.CurrentData.AllTables
        'Application.ExportXML acExportQuery, obj.Name, , path & "Tables\" & obj.Name & ".xsd"
    'Next
   
    For i = 0 To dbs.QueryDefs.Count - 1
        Application.SaveAsText acQuery, dbs.QueryDefs(i).Name, path & "Queries\" & dbs.QueryDefs(i).Name & ".sql"
    Next i
   
    'For i = 0 To dbs.TableDefs.Count - 1
    '    Application.ExportXML acExportTable, dbs.TableDefs(i).Name, , path & "Tables\" & dbs.TableDefs(i).Name & ".xsd"
    'Next i
   
    Set doc = Nothing
    Set cnt = Nothing
    Set dbs = Nothing
   
    MsgBox "Done"
Exit_DocDatabase:
    Exit Sub
   
Err_DocDatabase:
    Select Case Err

    Case Else
        MsgBox Err.Description
        Resume Exit_DocDatabase
    End Select
End Sub

Thursday, August 11, 2016

ASP.NET: 401 unauthorized access is denied due to invalid credentials

Solution 1: (this will end up with @User.Identity.IsAuthenticated be empty string)
  • In IIS, select the website you're having the issue with.
  • In the IIS section, select 'Authentication'.
  • Right click on Anonymous Authentication and select 'Edit'
  • Check the 'Specified User' radio button and click on 'Set'
  • I entered my username, password and password confirmation and click ok.

ASP.NET: Identity not using Windows NT

Things to check
1) what is set in the web.config, windows authentication?
 mode="Windows" />

    users="?"/>
impersonate="false" />
2) Is anonymous access in IIS disabled? (enable Windows Authentication)
3) Try HttpConext.Current.User instead of Thread.CurrentPrincipal

ASP.NET: Login failed for user 'DOMAIN\MACHINENAME$'

Symptom: 












Root cause: web application is using the account which doesn't have access to connect to SQL server

Solution:

  1. add "NT AUTHORITY\NETWORK SERVICE" as SQL Server login, allow it to access the database (in the "User Mapping", assign the db_owner role for the corresponding databases)
  2. in IIS, create a custom application pool which is using "NetworkService" as Identity (make sure to use Integrated pipeline). then change the web application to use this pool

Tuesday, May 3, 2016

DISTINCT and GROUP BY... and why does it not work for my query?

0. Introduction

Often, when running a query with joins, the results show up "duplicates", and often, those duplicates can be "eliminated" in the results using DISTINCT, for example.
Using DISTINCT is simple: just add it after the SELECT keyword, and you don't get duplicates. Unless you expect something else of what is a duplicate (and what not) than SQL does actually do. 
DISTINCT will not return two rows with the same values. To make it clear: it will compare the returned columns for the SELECT it is applied to, and not to the full table/join. The user problem is usually that they "expect" SQL to apply the DISTINCT only for one (or more) key fields, for example the first column returned in the select.
Usually, GROUP BY can solve this, but it might not be the most efficient method, or fail to accomplish other issues with the requirements in the query.
The good method to solve the problem is to step back, and look at both the data and the requested output, which, once clarified, can be translated into SQL Query "easily".

1. Tables and Data

We usually have 2 tables in the scenario:
 one master/parent table,
 one dependant table, linked through a 1:n relationship, with information such as history, traffic, accounting, etc information.
Notes:
 a related table is usually referred to as child table.
 a related table should have, as all tables, its own primary key.
In real-world, the field employee_pk would be indexed to ensure optimal performance.
For visualizing, here the data, queried using the SQL Server Management Studio 2005:
Employees:Employees
Work Records:Work RecordsIf you have trouble with dates/times, please refer to this article.

2. What is the exact requirement?

Request: Presume we want to see the last work day, per employee.
For Smith and Brown, we can forecast no major problems, but Bond lists 2 records for the last day...
Hence, you have to clarify: 
 do we just want the last record (date+time), or any record for the last date (ignoring time)?
 if all "duplicates" have the same value, is there another column to discriminate them, so we can decide on which one to take?
 if there are multiple records to be taken, what is the result we want to have?

Important:
This "problem" has to be solved first. Once the results from child table are OK, we can then join those results to the parent table (see step 7)

In our example, we could say: 
 A: give me just the last record, per employee, considering date + timeResult A
 B: give me all records of the last day, per employeeResult B
 C: give me the last date, per employee, with the earliest time for the work-start field, but the latest time for the work-end field
 Result C

As you see, I showed the expected result data, and NOT the code yet; and all the results are different.
We will in the next steps show the SQL to achieve those results, in the different engines.

3. This might be fine... but usually is not

To get this result: Result_0.JPG, you just need to run this query, works across all databases (which is the only + for this syntax):
select employee_pk
 , max(work_start_dt) last_start
 , max(work_end_dt) last_end
 , max(pk) last_pk
from tbl_Employee_WorkRecords
group by employee_pk;
 Open in new window
The query is very close to what is requested in A,  except for the fact that the individual results aren't synchronized:
 the value of max(pk) does not necessarily match the max(work_start_dt) or max(work_end_date)!
 if you had other columns you wanted to show, you would have the same issue as for pk, and using max() or min() shall not give the correct results

4. Result A

So, let's take this query, which is using the correlated subquery technique, which works also for alldatabases:
select t.* 
  from tbl_Employee_WorkRecords t
 where t.work_start_dt = 
    ( SELECT MAX(i.work_start_dt)
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    );
 Open in new window
A different syntax you will come across often is this technique:
select t.* 
  from tbl_Employee_WorkRecords t
  join ( select employee_pk, max(work_start_dt) max_start_dt
           from tbl_Employee_WorkRecords
          group by employee_pk
       ) i
    on i.employee_pk = t.employee_pk
   and i.max_start_dt = t.work_start_dt
 Open in new window

So, two working techniques, all fine, you could say, what else do we need? 
The issue is this: in our sample data, there are no real duplicates (date+time).
Let me show the results, with this MS SQL server query, letting the query ignore the time portion:
--- MS SQL Server
select t.* 
  from tbl_Employee_WorkRecords t
 where convert(varchar(10), t.work_start_dt, 120) = 
    ( SELECT convert(varchar(10), MAX(i.work_start_dt), 120)
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
     
You can see that if we had duplicates, the query would result also in duplicates. Both techniques shown have the same weakness!
To solve this, we need a unique row identifier. In my table, we could use the PK field, and the correlated query syntax becomes:
-- ms sql server
select t.* 
  from tbl_Employee_WorkRecords t
 where t.pk = ( select top 1 i.pk 
            from tbl_Employee_WorkRecords i
           where i.employee_pk = t.employee_pk
           order by i.work_start_dt DESC        
        )
 Open in new window
The main problem is that the pk field should only identify the row, and not have any other meaning, like ordering the results.

For MySQL Server, at least until the latest version documentation I have which is 5.4, I have bad news:
ORDER BY + LIMIT in a correlated subquery is not supported
http://dev.mysql.com/doc/refman/5.4/en/subquery-errors.html

For Oracle, we will have to say more or less the same: the TOP 1 from MS SQL Server cannot be implemented simply like this:
-- oracle => incorrect code
select t.* 
  from tbl_Employee_WorkRecords t
 where t.pk = ( select i.pk 
            from tbl_Employee_WorkRecords i
           where i.employee_pk = t.employee_pk
             and rownum = 1
           order by i.work_start_dt DESC         
        )
 Open in new window
Reason: Oracle applies the WHERE ROWNUM = 1 before the ORDER BY.
You might find some subquery in subquery syntax, but honestly: don't try, just use the much easier and still efficient method shown now:
-- oracle
select sq.* 
  from ( SELECT t.* 
              , ROW_NUMBER() OVER ( PARTITION BY employee_pk ORDER BY work_start_dt DESC ) rn
           FROM tbl_Employee_WorkRecords t
        ) sq
 WHERE sq.rn = 1
 Open in new window
When you remove the last condition WHERE sq.rn=1, you will see the additional column rn in the results, which should be self-explaining:Result DThe same syntax will actually work in MS SQL Server 2005 or higher, but not in SQL 2000 or lower, though.
I will not discuss the performance of this syntax here; you can check this out for yourself using the execution plans/explain plans and timing the queries.
I have seen no major problems with either syntax, so far, in my applications.

5. Result B

We now want all records for the last day. In the previous section, I have already suggested a method for MS SQL Server, i.e. using CONVERT() to ignore the time portion.
 Let's look at it again, more closely:
--- MS SQL Server
select t.* 
  from tbl_Employee_WorkRecords t
 where convert(varchar(10), t.work_start_dt, 120) = 
    ( SELECT convert(varchar(10), MAX(i.work_start_dt), 120)
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
     
The main issue is performance, as this part:
where convert(varchar(10), t.work_start_dt, 120) = (subquery) 
will make it impossible to use an index on work_start_dt (unless you used oracle with a functional-based index, but that's another discussion)

A slight optimisation can be achieved by adding a predicate not using a function, at least on the outside of the sub-query (explanation inline), which gives the somewhat awkward query:
--- MS SQL Server
select t.* 
  from tbl_Employee_WorkRecords t
 -- the work_start_dt needs to be higher than the max(work_start_dt) for this employee, -1 day
 where t.work_start_dt >= 
    ( SELECT DATEADD(DAY, -1, MAX(i.work_start_dt))
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
  -- ensure the record(s) are on the same date, actually, and not on the previous day than MAX(work_start_dt)
  AND convert(varchar(10), t.work_start_dt, 120) = 
    ( SELECT convert(varchar(10), MAX(i.work_start_dt), 120)
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
The same technique will work for MySQL, you only need to replace the CONVERT() expression, for example using the TO_DAYS function:
--- MySQL
select t.* 
  from tbl_Employee_WorkRecords t
 -- the work_start_dt needs to be higher than the max(work_start_dt) for this employee, -1 day
 where t.work_start_dt >= 
    ( SELECT DATEADD(DAY, -1, MAX(i.work_start_dt))
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
  -- ensure the record(s) are on the same date, actually, and not on the previous day than MAX(work_start_dt)
  AND TO_DAYS(t.work_start_dt) = 
    ( SELECT TO_DAYS(MAX(i.work_start_dt))
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
And in Oracle, the function would be called TRUNC(), which leads to a much better solution:
--- Oracle
select t.* 
  from tbl_Employee_WorkRecords t
 -- the work_start_dt needs to be higher than the max(work_start_dt) for this employee, -1 day
 where t.work_start_dt >= 
    ( SELECT TRUNC(MAX(i.work_start_dt))
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
MS SQL Server doesn't know how to truncate a date the same way, but we can use for example:
--- MS SQL Server
select t.* 
  from tbl_Employee_WorkRecords t
 -- the work_start_dt needs to be higher than the max(work_start_dt) for this employee, -1 day
 where t.work_start_dt >= 
    ( SELECT CONVERT(DATETIME, CONVERT(VARCHAR(10), MAX(i.work_start_dt), 120), 120)
        from tbl_Employee_WorkRecords i
       where i.employee_pk = t.employee_pk
    )
 Open in new window
To save space, I won't repeat the same method again, just mention the 2 functions you will like to use:str_to_date() and date_format()

But, we have even another bullet to solve this with SQL 2005+ and Oracle.
You have seen in Step 4 the ROW_NUMBER() function. We could use the RANK or DENSE_RANK function also, modifying the ORDER BY argument a little bit:
-- oracle
select sq.* 
  from ( SELECT t.* 
              , RANK () OVER ( PARTITION BY employee_pk ORDER BY TRUNC(work_start_dt) DESC ) rn
           FROM tbl_Employee_WorkRecords t
        ) sq
 WHERE sq.rn = 1
 Open in new window
Explanation: RANK() and DENSE_RANK() will hence give a rn=1 for all the values of the same DATE (as the time will be truncated => ignored).
For MS SQL Server, you need to use CONVERT(VARCHAR(10), work_start_dt, 120) instead ofTRUNC(work_start_dt), obviously.

6. Result C

Now that we have seen Result B, for request C this should be easy, as option B returns already the records we need. 
In short, Result C is a variation of Result B, if you look closely at it.

7. Combine with master table

Once you found the correct rule to determine which row you want to take from the child table, you can take that query, and join to the master table. If the child table might return no records, it shall be aOUTER JOIN instead of INNER JOIN.
So, here a sample in Oracle:
-- oracle
select sq.*, e.Name 
  from ( SELECT t.* 
              , ROW_NUMBER() OVER ( PARTITION BY employee_pk ORDER BY work_start_dt DESC ) rn
           FROM tbl_Employee_WorkRecords t
        ) sq
  join tbl_Employee e
    ON e.pk = sq.employee_fk
 WHERE sq.rn = 1
 Open in new window

For performance reasons, for example if the master table has a condition, you might want to join in the subquery directly, giving below code. The example returns all employees with lastname starting with 'B'.
Watch closely the condition sq.rn = 1 has to be part of the left join condition, and NOT of the wherecondition! If that condition was moved into the where, you make the query behave like a inner join.
-- oracle
select sq.*, e.Name 
  from tbl_Employee e
  left join ( SELECT t.* 
              , ROW_NUMBER() OVER ( PARTITION BY t.employee_pk ORDER BY t.work_start_dt DESC ) rn
           FROM tbl_Employee e
           JOIN tbl_Employee_WorkRecords t
             ON e.pk = sq.employee_fk
           WHERE e.last_name like 'B%'
 
        ) sq
    ON e.pk = sq.employee_fk
   AND sq.rn = 1 
  WHERE e.last_name like 'B%'
 Open in new window


8. Conclusion

Once you clearly identified the needs for the output data, in regards to the input, the SQL can be written with above templates easily, just a matter of practice. Usually, you don't use DISTINCT or GROUP BY for these scenarios, either.

I added a link about UNION vs. UNION ALL, because UNION used alone implicitly performs a DISTINCT, something that most newcomers in SQL just don't know... and wonder about the query running slowly.

Happy coding!

Analytical functions:
http://www.experts-exchange.com/articles/Database/Miscellaneous/Analytical-SQL-Where-do-you-rank.html

DISTINCTROW <> DISTINCT
MS Access : http://office.microsoft.com/en-us/access/HP010322051033.aspx


UNION vs. UNION ALL
SQL server : http://blog.sqlauthority.com/2009/03/11/sql-server-difference-between-union-vs-union-all-optimal-performance-comparison/
Oracle         : http://www.oraclebrains.com/2007/09/sql-tipsuse-union-all-instead-of-union/
MySQL        : http://www.mysqlperformanceblog.com/2007/10/05/union-vs-union-all-performance/

Reference: https://www.experts-exchange.com/articles/3203/DISTINCT-and-GROUP-BY-and-why-does-it-not-work-for-my-query.html