Common Table Expressions – The Building Blocks of SQL

Common Table Expressions (CTE) are a feature that was first introduced to SQL in about 2005 and provide a method of defining a temporary result set then using that in the query as if it were another table.

Yes, subqueries do the same thing but a CTE can be used repeatedly in the query after it has been declared without further re-declaration whereas a sub-query has to be declared every time it is used. Views could also be created but that involves a physical object within the database being created and remembering to tidy up and delete it later.

The CTE is declared at the start of a query using the WITH construct so I can write:

WITH pers AS
(
Select
         NameTable.OwnerID AS RIN1,
         NameTable.Surname COLLATE NOCASE AS Surname,
         NameTable.Given  COLLATE NOCASE AS Given
FROM NameTable		 
where NameTable.IsPrimary = 1
)

The very first word of the first CTE has to be WITH and this has to be the very start of the query, apart from comments. Then the name of the CTE is declared folowed by AS then its definition in brackets. CTE names are best kept short as they will be used further on in the query. Now I have an item I can use in the rest of my query so that continues:

SELECT 
EventID, 
EventType,
pers.RIN1,
pers.Surname,
pers.Given
FROM EventTable as ev
LEFT OUTER JOIN pers ON
ev.OwnerID = pers.RIN1 
AND ev.OwnerType = 0

so you can see pers is treated as if it were another table or view in the query.

This is just a simple example using a CTE once. A query can contain multiple CTE and once the first has been declared, subsequent CTE are declared by continuing the WITH clause and putting
, ctename AS
(
my cte code
)
rather than another WITH. So I can add another CTE to get both parent details for a family and this can (but does not have to) use the first CTE.

,fam as
(
select FamilyID, RIN1, Surname, Given
from FamilyTable f inner join pers
on f.FatherID = pers.RIN1
where f.FatherID > 0
UNION
select FamilyID, RIN1, Surname, Given
from FamilyTable f inner join pers
on f.MotherID = pers.RIN1
where f.MotherID > 0
)

All this happens before the main query and the main query simply uses those CTE as if they were tables.
So main query could now be

SELECT 
EventID, 
EventType,
f.Name COLLATE NOCASE AS FactType,
ev.OwnerType,
ev.OwnerTypeN, 
case when ev.OwnerType = 1 THEN fam.Surname
	ELSE pers.Surname
	END as Surname,
case when ev.OwnerType = 1 THEN fam.Given
	ELSE pers.Given
	END as Given,
case when ev.OwnerType = 1 THEN fam.RIN1
	ELSE pers.RIN1
	END as RIN1,
ev.Eventdate,
ev.proof,
ev.placeID,
ev.SiteID, 
ev.details
from ev /* join in person where owner type is person */
INNER JOIN FactTypeTable f ON
         e.EventType = f.FactTypeID
LEFT OUTER JOIN pers ON
         ev.OwnerID = pers.RIN1 and ev.OwnerType = 0
/* join in the family persons for owner type = family */
LEFT OUTER JOIN fam ON
         ev.OwnerID = fam.FamilyID and ev.OwnerType = 1
WHERE    ev.EventType <> 35 /* ignore reference numbers */
/* and (ev.Proof = 0) – not proven (remove surrounding comment markers /* and */ to include this restriction)*/
ORDER BY Surname, given, RIN1, eventtype

The overall layout is
WITH a AS (something) , b AS (something else) , c AS (something more) SELECT ……….

Note the left outer join syntax which allows all records from the left hand table to be included even if there is not a matching record on the right hand side. Missing data is set to null values. Inner JOIN and just plain JOIN would leave out records which do not have a match for the person id and an owner type of 0 and in fact the whole query would just mess up.

A good rule for SQL readability is to always fully specify the join type so never use just JOIN but always INNER JOIN or LEFT OUTER JOIN.
There is also a RIGHT OUTER JOIN and a FULL OUTER JOIN but SQLite does not support these constructs.

Another useful implementation of the CTE is to expand out tables in RM that contain dates and fields that need collation changes. The date expansion code is longwinded and distracting from the main query. Putting it in a CTE gets it out of the way, leaving the main query readable.

My full query example attached is a list of facts and their citations, including the owner (person or family). By altering the where clause this can list facts without citations or facts not marked proven (or both).

With the database changes between RM7 and RM8 the CTE is most useful to convert queries written for version 7 to run against a version 8 database. The CitationTable from version 7 has been split into CitationTable and CitationLinkTable in version 8 but using a CTE we can emulate the version 7 structure then just change the table name in the main query.

So we have:

With Cit8 as
(Select c.CitationID, cl.OwnerType, cl.OwnerID, c.SourceID, cl.Quality, cl.IsPrivate, c.Comments, c.ActualText, c.RefNumber, cl.Flags, c.Fields
from CitationTable c INNER JOIN CitationLinkTable cl
ON c.CitationID = cl.CitationID)

then continue with original query but change all occurrences of CitationTable to Cit8
So a simple list of citations with their sources becomes:

With Cit8 as
(Select c.CitationID, cl.OwnerType, cl.OwnerID, c.SourceID, cl.Quality, cl.IsPrivate, c.Comments, c.ActualText, c.RefNumber, cl.Flags, c.Fields

from CitationTable c INNER JOIN CitationLinkTable cl
ON c.CitationID = cl.CitationID)
SELECT * FROM Cit8 LEFT OUTER JOIN SourceTable ON CitationTable.SourceID = SourceTable.SourceID

I have built queries with many CTE to progressively build up data for complex reporting and find it a far more efficient way of coding which can be easily tested level by level.

My query to list facts and their citations is attached in two versions, cte-facts-citations-RM7.sql for RootsMagic version 7 databases
and cte-facts-citations-RM8.sql for RootsMagic version 8 databases.

Further Reading:

This article is aimed at SQL Server but SQLite uses the same syntax.
https://www.red-gate.com/simple-talk/sql/t-sql-programming/sql-server-cte-basics/

Pat Jones
SQL and Business Intelligence Developer

Comparing #Database Definitions RM8 versus RM7 #datadefinitions

Comparing the CREATE statements for a database file of each version reveals changes to persisting tables in addition to tables deleted and added per First Look at RM8 #Database. This post provides DDL files from RM7.6.5 and the RM8 Preview 7.9.101. The two files can be compared line by line in Notepad++ using the Compare plugin, a screenshot of which is shown below.

By rearranging the order of statements in the two different files to juxtapose tables whose names have changed, the coloured comparison is even more effective. The next screenshot shows how RM7’s LabelTable has been transformed to RM8’s TagTable. RM7’s ResearchItemTable and ResearchTable have been consolidated into RM8’s TaskTable with an indirection via the new TaskLinkTable.

Comparison after rearranging DDL files for improved juxtapositioning of similar statements.

These graphical comparisons help in finding the more subtle changes that may imply things to watch out for. For example, why has SortDate been changed from Integer to BigInt?

First Look at RM8 #Database

More changes than expected with the result that many (most?) of the queries and scripts published on this site won’t work with RM8 databases.

New Tables

CitationLinkTable: was expected to support the announced new feature of master citations.

AncestryTable and FamilySearchTable: replace the former LinkAncestryTable and LinkTable, now gone.

TagTable: looks to have replaced LabelTable, now gone.

TaskTable and TaskLinkTable: appear to have replaced ResearchTable and ResearchItemTable, now gone.

Global Change

Every Table has, as its last column, a new column named UTCModDate, apparently intended to track the date on which a record was added or last changed.

Query to Search Parents by Children’s Given Names #search #family

File above incorporates revision from discussion below to correct the count of children. Tested with #rm7 #rm8 and should be compatible with prior versions. TH 2021-03-03

The following script was developed in response to the discussion Search for family by given names of children (was RM-8)

-- three_childrens_names.sql
-- Jerry Bryan, 22 May 2020
-- This query searches for parents based on given names of their children.
-- It gets a hit when three or more children have any primary or alternate
-- given names that are in a hardwired list. The report lists the parents.

SELECT COUNT(C.ChildID) AS fam_size,
F.FatherID, father.Given, father.Surname, F.MotherID, mother.Given, mother.Surname
FROM ChildTable AS C
JOIN
(SELECT DISTINCT NN.OwnerID
FROM NameTable AS NN
WHERE NN.Given LIKE("%Mary%") OR NN.Given LIKE ("%John%") OR NN.Given LIKE("%Elizabeth%"))
AS N ON N.OwnerID = C.ChildID
JOIN
FamilyTable AS F ON F.FamilyID = C.FamilyID
JOIN
NameTable AS father ON father.OwnerID = F.FatherID
JOIN
NameTable AS mother ON mother.OwnerID = F.MotherID
GROUP BY C.FamilyID
HAVING fam_size >= 3
ORDER BY father.Surname, father.Given, mother.Surname, mother.Given;

Jerry

Aargh! Systemic Error re: GroupID

I’ve just discovered that I caused a systemic error in several scripts dealing with Groups in relating the LabelTable.LabelID to the GroupTable.GroupID when the latter is actually related to LabelTable.LabelValue. It’s of no consequence when groups are consecutively created but not deleted because the LabelID and LabelValue match. The problem arises when a group before the latest created is deleted; the LabelID for a subsequently created group could point to the wrong or a non-existent GroupID.

Over the next while, I will try to find and correct those scripts. A starting point for identifying them would be a site search using both terms “LabelID” and “GroupID”.

UPDATE 2020-05-16: not as bad as I feared. The original error was corrected in many but not all scripts in 2011. Here are the exceptions that have been found and are now corrected:

Copy RIN to REFN and embedded script
Copy Fact to Group and CopyFact2Group.sql
Census Needed: Named Group and CensusNeededGroup2.sql

Names – Set Alternate of type Birth to Primary #names #alternatenames #birth

This script does two things:

  1. For those persons having one Alternate Name of type “Birth” and a Primary name of “undefined” type, it sets the Alt name as the Primary name and changes the previous Primary name to an Alternate Name.
  2. For those persons having more than one Name of type “Birth”, it lists the Record Number (RIN) and Surname and Given Names so that the user may resolve the ambiguity down to one so that the script may swap it on the next pass.

If the conclusion is that the current Primary (birth) name is the preferred name, the user may choose to delete the Alternate Name, modify its name type or leave it as is. Either way, the script will not change the Primary to Alternate unless the Primary type is “undefined”.

Sources and media remain attached to the name, whatever its Primary/Alternate state is. However, until RM8, those attached to the Primary Name are essentially inaccessible and unusable (see Citations Invisible – Reveal and Revert).

Download

Names-Swap_AltBirth_to_Primary.sql

 

Combining the contents of 2 queries on the same file

I am having trouble setting up this query.

I want the results of the first query to merge/join/union with the second query where the ownerid from the first query equals the ownerid from the second query. The bold line should not be part of the query. I put it in to get the records from the second that equal the ownerid from the first query.

Select n1.ownerid, n1.surname, n1.given, n1.nametype, n1.prefix, n1.isprimary

from nametable n1

where n1.nametype = 1

and n1.isprimary = 0

order by surname, given

I get the results in Pic 1

select n2.ownerid, n2.surname, n2.given, n2.nametype, n2.prefix, n2.isprimary

from nametable n2

where n2.nametype = 0

and n2.isprimary = 1

and ownerid in(10518, 10519, 11626, 11976, 3536, 11805, 10773, 11655, 16605, 11802, 255, 129, 130, 15721, 9046, 11495, 11531)

order by surname, given

I get the results in Pic 2

I want the results of query 1 to be combined with the results of query 2 where the ownerids from the first query are only the ownerids of the second query.

So I get something like Pic 3

Privatise Extra BMD Events #events #reports #private

Problem

Many persons in my database have multiple Birth events created from different sources; likewise for other vital facts. Even though I may have marked one as Primary, the others still clutter reports: narratives look like repetitious gobbledygook; calendars have the same person multiple times, even on the same day. I want to clean up the publishing mess without losing the evidence.

This is especially the case for databases downloaded from an Ancestry Member Tree where events are generated by accepting or assigning a source and choosing to make the data into an alternate fact because it varies in some way from an existing fact.

Solution

Many report settings in RootsMagic have an option to include or exclude private events. I could reduce report clutter by setting the non-Primary Birth events to Private without losing the data that led to their creation. The Primary event should have whatever consideration and logic went into it being deemed the most accurate. I haven’t set a Primary in every case where my database has multiples so I want two things:

  1. A batch process that sets Private all the non-Primary events of a set having a Primary.
  2. A list of all the sets of multiple events that do not have a Primary so I can follow up.

Individual Summary with multiple Birth events, highlighted.

Individual Summary excluding Private events, in this case, the extra Birth events.

Implementation

BMD_private_multiples.sql 2020-02-15 Click to view; right-click to download.

The above SQLite script builds a series of temporary SQLite Views (stored queries), does the possible privatising and exits displaying the View “BMDmultstofix” which lists the persons needing follow up attention to set Primary one of the plural (multiple) events. The Views are all in memory and are lost when the SQLite manager exits the database.

Usage

The script executes very quickly on small files so it is possible to keep the SQLite manager open on the database and to re-execute the script at intervals while editing the database in RootsMagic. One could simply set the Primary event out of a group of multiple events for a person in RM’s Edit Person screen and then run the script to set the others Private or manually set them. Re-running the script will shrink the list needing Primaries by those you have set.

For family-type events (Marriage), the View “BMDmultstofix” will list both spouses separately. When the Marriage event is set to Primary on either spouse, the two separate listings will go.

Possible Enhancements

Colour Coding

The list of persons needing attention could be quite large, depending how assiduously you have been setting the Primary flag and the number of people and events in your database. So it might be nice to extend the last query or even last View to pick up the color code for each person which could be used to sort or filter the list.

Private as Default

Instead of privatising only those non-Primary events in groups having a Primary, another approach would be to privatise all non-Primaries. That would really clean out the reports and require user editing to get the desired outputs. This may be preferable.

Enhanced Version

BMD_private_multiples2.sql 2020-02-16

Both of the hypothesized enhancements have been incorporated. To ‘fix’ a group of multiples, one of the events must be set Primary and un-Privatised or another event added that consolidates one’s research into an assertion. By default, a new event is not Private and need only be made Primary.

In SQLiteSpy, any column can be sorted by clicking on the heading. Some other software, SQLite Expert is one example, also support filtering by column. These tools enable grouping color code or by surname without having to modify the script or write your own custom query of BMDmultsToFixClr view.

Screenshot

Screenshot from SQLiteSpy of results from running BMD_private_multiples2.sql