“I have some records in a table which are duplicates. In this case, I can only identify “duplicate records” by whether several fields are identical; not just one. For example, let’s say I want to identify Incidents where the Short description, Assignment group, and Assigned to user are all the same. If only one or two of those fields match, but one of them is different, then the records are not duplicates. The’re only duplicates if all three fields are the same.
Is there any way to accomplish this with a script? Maybe by using
GlideAggregate
?”
Thanks for your question, and indeed, there is a way to identify duplicate records based on multiple matching fields in a script in ServiceNow!
At first glance, you might think that the GlideAggregate APIs do not support any direct mechanism by which to identify duplicate records across multiple identical fields. However, there is a way!
By getting just a smidge clever, we can call the .groupBy()
API method multiple times, in combination with the .addAggregate()
and .addHaving()
methods, to accomplish our goal of identifying duplicate records with more than one field that are identical.
Consider the following code, for example - which is a basic example of how to find duplicates in a table based on multiple fields:
Note: This is not the optimal solution. There is far more optimal and functional code at the end of this article. If you just came here for the solution and want to skip the the learning, you can click here.
var dupeCount = 0;
var gaMyTableDuplicate = new GlideAggregate('incident');
gaMyTableDuplicate.addAggregate('COUNT');
gaMyTableDuplicate.groupBy('short_description');
gaMyTableDuplicate.groupBy('assignment_group');
gaMyTableDuplicate.groupBy('assigned_to');
gaMyTableDuplicate.addHaving('COUNT', '>', 1);
gaMyTableDuplicate.query();
while (gaMyTableDuplicate.next()) {
dupeCount = gaMyTableDuplicate.getAggregate('COUNT');
gs.info(
dupeCount + ' records found with the following identical values:\n' +
'•Short description: ' + (gaMyTableDuplicate.getValue('short_description') || '(blank)') + '\n' +
'•Assignment group: ' + (gaMyTableDuplicate.assignment_group.getDisplayValue() || '(blank)') + '\n' +
'•Assigned to: ' + (gaMyTableDuplicate.assigned_to.getDisplayValue() || '(blank)') + '\n\n'
);
}
This basic method of using the GlideAggregate .groupBy()
method allows us to group by fields, count groups matching on those fields, and filter where the groups have more than one record in them (aka, duplicates!)
When we execute this in a background script, we get results printed out which look something like this:
5 records found with the following identical values: •Short description: (blank) •Assignment group: Team Development Code Reviewers •Assigned to: Tim Woodruff
Cool, that works!
Now let's drop it into a function we can call, and add some arguments, and some input validation.
function getMultiFieldDupes(tableName, arrDupeFields) {
var iDupeField, gaMyTableDuplicate;
var dupeCount = 0;
if (typeof tableName !== 'string' || !tableName) {
throw new Error(
'getMultiFieldDupes(): tableName must be a string consisting of a valid ' +
'table name in the ServiceNow database.'
);
}
if ( //If arrDupeFields is not an array or a string, or if it's an array but it's empty
typeof arrDupeFields === 'undefined' ||
!arrDupeFields ||
(!Array.isArray(arrDupeFields) && typeof arrDupeFields !== 'string') ||
!arrDupeFields.length
) {
throw new Error(
'getMultiFieldDupes(): arrDupeFields must be a string with a single ' +
'field name, or an array of strings - each string representing a ' +
'field name in the ' + tableName + ' table.'
);
}
//If arrDupeFields is a string, convert it to an array.
if (typeof arrDupeFields === 'string') {
arrDupeFields = arrDupeFields.split(',');
}
gaMyTableDuplicate = new GlideAggregate(tableName);
gaMyTableDuplicate.addAggregate('COUNT');
//Group by each field in the arrDupeFields array
for (
iDupeField = 0;
iDupeField < arrDupeFields.length;
iDupeField++
) {
gaMyTableDuplicate.groupBy(arrDupeFields[iDupeField]);
}
gaMyTableDuplicate.addHaving('COUNT', '>', 1);
gaMyTableDuplicate.query();
while (gaMyTableDuplicate.next()) {
dupeCount = gaMyTableDuplicate.getAggregate('COUNT');
gs.info( //todo: replace with dynamic field values - but whatever, this is just an intermediary example.
dupeCount + ' records found with the following identical values:\n' +
'•Short description: ' + (gaMyTableDuplicate.getValue('short_description') || '(blank)') + '\n' +
'•Assignment group: ' + (gaMyTableDuplicate.assignment_group.getDisplayValue() || '(blank)') + '\n' +
'•Assigned to: ' + (gaMyTableDuplicate.assigned_to.getDisplayValue() || '(blank)') + '\n\n'
);
}
}
Now we can call this function with a table name and an array of fields to group by, and it will return pretty much the same results as before.
Next, let's add some additional features and functionality.
I noticed that with the first version of our code, we were outputting records where some of the fields (such as the short description) were blank.
This may be fine, but I can imagine a scenario in which we may not want that.
Therefore, let's add a feature to allow us to filter out records where any of a specified (optional) list of fields is blank.
Since we may want to get the sys_ids for each duplicate record within a given duplicate "type" (each specific combination of unique field values), let's also add a feature to return an array of sys_ids for each duplicate record found within each duplicate "type".
And finally, rather than printing it out, let's have the function return an array of objects with details about each set of duplicates found.
Optimal Solution
This solution supports specifying one or more field(s) you want to compare for duplicates (yes, it works with only a single dupe-field as well), specifying an optional list of fields to filter out if they're blank, and even optionally getting a list of sys_ids for all of the records which match each duplicate scenario!
/**
* Get records with duplicate values in multiple fields, and return a list of the records'
* counts and the dupe fields' values.
*
* @param {string} tableName - The table to check for duplicates. Must be a string.
*
* @param {string|string[]} arrDupeFields - The fields to check for duplicates.
* Must be a string or an array of strings.
*
* @param {string|string[]} [nonBlankFields=[]] - This optional parameter, if specified, will
* be an array of strings consisting of the names of the fields which should not be allowed
* to be blank when checking for duplicates.
* To put it another way, if any of the fields specified in the `nonBlankFields` array are
* blank, then the record should not be considered a duplicate.
* Note that the fields specified in nonBlankFields do NOT need to also be specified in
* arrDupeFields.
*
* @param {boolean} [getDupeSysIds=false] - If true, the sys_ids of the records with the
* duplicate values will be included in the returned object. If false, they will not be
* included.
* This may have a performance impact, as it requires an additional query for each
* duplicate combination, so use with caution.
* Default is false.
*
* @returns {{dupeCount: (*|number), fields: {}}[]} - An array of objects, each object
* representing a confluence of duplicate values in the specified fields - a specific
* combination of field values - and the number of records that have that combination of
* those values in the specified fields.
*/
function getMultiFieldDupes(tableName, arrDupeFields, nonBlankFields, getDupeSysIds) {
var gaMyTableDuplicate, grDupe, iNonBlankField, iDupeField, objDupeRecord;
var arrDupeRecords = [];
var dupeCount = 0;
/***** INPUT VALIDATION *****/
getDupeSysIds = (typeof getDupeSysIds === 'boolean') ? getDupeSysIds : false;
if (typeof tableName !== 'string' || !tableName) {
throw new Error(
'getMultiFieldDupes(): tableName must be a string consisting of a valid ' +
'table name in the ServiceNow database.'
);
}
if ( //If arrDupeFields is not an array or a string, or if it's an array but it's empty
typeof arrDupeFields === 'undefined' ||
!arrDupeFields ||
(!Array.isArray(arrDupeFields) && typeof arrDupeFields !== 'string') ||
!arrDupeFields.length
) {
throw new Error(
'getMultiFieldDupes(): arrDupeFields must be a string with a single ' +
'field name, or an array of strings - each string representing a ' +
'field name in the ' + tableName + ' table.'
);
}
//If arrDupeFields is a string, convert it to an array.
if (typeof arrDupeFields === 'string') {
arrDupeFields = arrDupeFields.split(',');
}
//If nonBlankFields is undefined, null, or an empty string, set it to an empty array.
//If it's a string, convert it to an array.
if (typeof nonBlankFields === 'undefined' || !nonBlankFields) {
nonBlankFields = [];
} else if (typeof nonBlankFields === 'string') {
//Splitting just in case the input data is a comma-separated string - which it
// shouldn't be, but I don't trust anyone who calls this code. They seem sus.
nonBlankFields = nonBlankFields.split(',');
} else if (!Array.isArray(nonBlankFields)) {
//If it's not a string or an array or undefined, throw an error because wth am I s'posed to do with that
throw new Error(
'getMultiFieldDupes(): nonBlankFields must be a string with a single ' +
'field name, or an array of strings - each string representing a ' +
'field name in the ' + tableName + ' table.'
);
}
/***** ACTUALLY DOING THE THING *****/
gaMyTableDuplicate = new GlideAggregate(tableName);
gaMyTableDuplicate.addAggregate('COUNT');
//Group by each field in the arrDupeFields array
for (
iDupeField = 0;
iDupeField < arrDupeFields.length;
iDupeField++
) {
gaMyTableDuplicate.groupBy(arrDupeFields[iDupeField]);
}
//If any nonBlankFields were specified, add a query to exclude records where
// any of those fields are blank.
for (
iNonBlankField = 0;
iNonBlankField < nonBlankFields.length;
iNonBlankField++
) {
gaMyTableDuplicate.addNotNullQuery(nonBlankFields[iNonBlankField]);
}
//Only show records with more than one match (duplicates)
gaMyTableDuplicate.addHaving('COUNT', '>', 1);
gaMyTableDuplicate.query();
while (gaMyTableDuplicate.next()) {
dupeCount = gaMyTableDuplicate.getAggregate('COUNT');
//Populate the arrDupeRecords array with some info about the records that have duplicates
objDupeRecord = {
"dupeCount": dupeCount,
"fields": {}
};
//For each field in the arrDupeFields array, add that field's value to
// the objDupeRecord.fields object.
for (
iDupeField = 0;
iDupeField < arrDupeFields.length;
iDupeField++
) {
objDupeRecord.fields[arrDupeFields[iDupeField]] = gaMyTableDuplicate.getValue(arrDupeFields[iDupeField]);
}
if (getDupeSysIds) {
objDupeRecord.dupe_sys_ids = [];
//Add the sys_ids of all the records that have this combination of dupe fields in objDupeRecord.dupe_sys_ids:
grDupe = new GlideRecord(tableName);
for (
iDupeField = 0;
iDupeField < arrDupeFields.length;
iDupeField++
) {
grDupe.addQuery(arrDupeFields[iDupeField], objDupeRecord.fields[arrDupeFields[iDupeField]]);
}
grDupe.query();
while (grDupe.next()) {
objDupeRecord.dupe_sys_ids.push(grDupe.getUniqueValue());
}
}
arrDupeRecords.push(objDupeRecord);
}
return arrDupeRecords;
}
Usage
Example 1: Duplicate Incidents with sys_ids
Calling the function in this way will get a list of field values and sys_ids for records which are duplicates based on the Short description, Assignment group, and Assigned to fields all matching. It will not count any records with a blank Short description or Assigned to field as duplicates.
var dupeRecords;
dupeRecords = getMultiFieldDupes(
'incident', //Table name
['short_description', 'assignment_group', 'assigned_to'], //Fields to check for duplicates
['short_description','assigned_to'], //Fields that must not be blank
true //Get the sys_ids of the records with duplicates
);
//Print out the results:
gs.info(
'Found ' + dupeRecords.length + ' duplicate types:\n' +
JSON.stringify(dupeRecords, null, 2)
);
Output:
[
{
"dupeCount": "2",
"fields": {
"short_description": "Watchlist ticket",
"assignment_group": "cfcbad03d711110050f5edcb9e61038f",
"assigned_to": "713e4bd0dba90110495d70f33996198b"
},
"dupe_sys_ids": [
"3c4cf41e1bd069900f679713b24bcbff",
"b44cb01e1bd069900f679713b24bcb36"
]
},
{
"dupeCount": "4",
"fields": {
"short_description": "Some Short Description",
"assignment_group": "cfcbad03d711110050f5edcb9e61038f",
"assigned_to": "713e4bd0dba90110495d70f33996198b"
},
"dupe_sys_ids": [
"0a8fe93a1b50e9900f679713b24bcbb6",
"0a9ab1041ba4ed900f679713b24bcb0d",
"134542be1b90e9900f679713b24bcba6",
"1511c27a1b90e9900f679713b24bcbb7"
]
}
]
Example 2: Users with duplicate first+last names
This function-call will get a list of users who have the same first and last name. It will not get the sys_ids of each duplicate user, but will give you the list of first+last names corresponding to duplicates.
var dupeRecords = getMultiFieldDupes(
'sys_user', //Table name
['first_name', 'last_name'],
null, //No fields must be non-blank
false //Don't get the sys_ids of the records with duplicates
);
gs.info(
'Found ' + dupeRecords.length + ' duplicate types:\n' +
JSON.stringify(dupeRecords, null, 2)
);
Output:
[
{
"dupeCount": "15",
"fields": {
"first_name": "",
"last_name": ""
}
},
{
"dupeCount": "2",
"fields": {
"first_name": "John",
"last_name": "Smith"
}
}
]
Example 3: Incidents with duplicate Short descriptions
Calling the function in this way will return the Incident Short descriptions which are duplicates. This is to demonstrate that this function works whether you specify one dupe field, or multiple.
This will also filter out any records with a blank Assignment group field, just to demonstrate that the blank field filter does not need to operate exclusively on a field which is included in your list of dupe-fields.
var dupeRecords = getMultiFieldDupes(
'incident', //Table name
['short_description'], //Field to check for duplicates
'assignment_group', //Field that must not be blank
false //Don't get the sys_ids of the records with duplicates
);
gs.info(
'Found ' + dupeRecords.length + ' duplicate types:\n' +
JSON.stringify(dupeRecords, null, 2)
);
Output:
[
{
"dupeCount": "5",
"fields": {
"short_description": ""
}
},
{
"dupeCount": "2",
"fields": {
"short_description": "Reset my password"
}
}
]
Thanks for reading! If you enjoyed this article or found this tool helpful, please consider sharing it on your favorite social media so others can find it too! If you want to see more from SN Pro Tips and Tim Woodruff, consider subscribing below or following Tim on LinkedIn!
-
2024
- Mar 28, 2024 How to Identify Duplicate Records by Multiple Fields in ServiceNow Mar 28, 2024
- Mar 7, 2024 How to Merge Personal & Company ServiceNow Accounts Mar 7, 2024
- Feb 12, 2024 5 Lessons About Programming From Richard Feynman Feb 12, 2024
-
2023
- Jul 5, 2023 Managing Instance-Specific System Properties for Dev/Test/Prod in ServiceNow Jul 5, 2023
- May 11, 2023 5 Ways to Check your ServiceNow Instance for DANGEROUS CODE in Less Than 5 minutes May 11, 2023
- Apr 28, 2023 Your ACLs and Business Rules are Broken (Here's How to Fix Them) Apr 28, 2023
-
2022
- Dec 13, 2022 ServiceNow Developers: BE THE GUIDE! Dec 13, 2022
- Oct 19, 2022 A Faster, More Efficient Client-side GlideRecord (Free tool!) Oct 19, 2022
- Oct 9, 2022 Animated Loading Message & Collapsible Details on ServiceNow Form or Field (Client-side) Oct 9, 2022
- Aug 23, 2022 Using .addJoinQuery() & How to Query Records with Attachments in ServiceNow Aug 23, 2022
- Aug 18, 2022 Free, Simple URL Shortener for ServiceNow Nerds (snc.guru) Aug 18, 2022
- Aug 16, 2022 How to Get and Parse ServiceNow Journal Entries as Strings/HTML Aug 16, 2022
- Aug 14, 2022 New tool: Get Latest Version of ServiceNow Docs Page Aug 14, 2022
- Mar 4, 2022 How to Set or Change ServiceNow Application's Repository URL, Credentials, or SSH Key Mar 4, 2022
- Feb 7, 2022 How to return a CSV file from a Scripted REST API (SRAPI) in ServiceNow Feb 7, 2022
-
2021
- May 3, 2021 Adding a Guided Setup to Your ServiceNow Application May 3, 2021
- Apr 27, 2021 Use Automated Tests to Validate "Guided Setup" Completion & Functionality. Apr 27, 2021
- Feb 11, 2021 "Processors", SRAPIs, and How to Run a Script and Redirect a User From a URL in ServiceNow Feb 11, 2021
-
2020
- Nov 17, 2020 SN Guys is now part of Jahnel Group! Nov 17, 2020
- Sep 14, 2020 Better ServiceNow Notifications (& Another FREE Tool!) Sep 14, 2020
- Jul 31, 2020 Debugging Client & Catalog Client Scripts in ServiceNow Jul 31, 2020
- Jan 20, 2020 Getting Help from the ServiceNow Community Jan 20, 2020
-
2019
- Dec 18, 2019 Can ServiceNow Script Includes Use the "current" Variable? Dec 18, 2019
- Nov 18, 2019 Handling 'text/plain' and Other Unsupported Content Types in ServiceNow Scripted REST APIs Nov 18, 2019
- Apr 21, 2019 Understanding Attachments in ServiceNow Apr 21, 2019
- Apr 10, 2019 Using Custom Search Engines in Chrome to Quickly Navigate ServiceNow Apr 10, 2019
- Apr 4, 2019 Set Catalog Variables from URL Params (Free tool) Apr 4, 2019
- Apr 1, 2019 Outlook for Android Breaks Email Approvals (+Solution) Apr 1, 2019
- Mar 11, 2019 GlideFilter is Broken - Free Tool: “BetterGlideFilter” Mar 11, 2019
- Feb 27, 2019 Making Update Sets Smarter - Free Tool Feb 27, 2019
-
2018
- Nov 29, 2018 How to Learn ServiceNow Nov 29, 2018
- Nov 6, 2018 ServiceNow & ITSM as a Career? Nov 6, 2018
- Oct 19, 2018 Asynchronous onSubmit Catalog/Client Scripts in ServiceNow Oct 19, 2018
- Oct 11, 2018 How to do Massive, Slow Database Operations Efficiently With Event-Driven Recursion Oct 11, 2018
- Sep 18, 2018 Broken Queries & Query Business Rules in ServiceNow Sep 18, 2018
- Sep 7, 2018 JournalRedactor - Easily Redact or Delete Journal Entries in ServiceNow! Sep 7, 2018
- Jul 23, 2018 Admin Duty Separation with a Single Account Jul 23, 2018
- Jun 19, 2018 Improving Performance on Older Instances with Table Rotation Jun 19, 2018
- Jun 4, 2018 New Free Tool: Login Link Generator Jun 4, 2018
- May 29, 2018 Learning ServiceNow: Second Edition! May 29, 2018
- Apr 17, 2018 Upgrading From Express to Enterprise: What's Missing Apr 17, 2018
- Apr 12, 2018 If a Genie Gave Me Three Wishes, I'd Use Them All to "Fix" Scope Apr 12, 2018
- Mar 19, 2018 Service Catalog "Try in Portal" button Mar 19, 2018
- Mar 15, 2018 Video: Custom Output Transition Conditions From a Single Workflow (Script) Activity Mar 15, 2018
- Feb 11, 2018 We have a new book! Feb 11, 2018
-
2017
- Nov 6, 2017 Requiring Attachments (& Other Miracles) in Service Portal Nov 6, 2017
- Sep 12, 2017 Handling TimeZones in ServiceNow (TimeZoneUtil) Sep 12, 2017
- Jul 27, 2017 How to Enable DOM Manipulation in ServiceNow Service Portal Catalog Client Scripts Jul 27, 2017
- Jun 25, 2017 What's New in ServiceNow: Jakarta (Pt. 1) Jun 25, 2017
- Jun 4, 2017 Powerful Scripted Text Search in ServiceNow Jun 4, 2017
- May 9, 2017 Work at Lightspeed: ServiceNow's Plan for World Domination May 9, 2017
- Apr 9, 2017 Avoiding Pass-By-Reference Using getValue() & setValue() Apr 9, 2017
- Apr 4, 2017 "Learning ServiceNow" is Now Available for Purchase! Apr 4, 2017
- Mar 12, 2017 reCAPTCHA in ServiceNow CMS/Service Portal Mar 12, 2017
-
2016
- Dec 20, 2016 Pro Tip: Use updateMultiple() for Maximum Efficiency! Dec 20, 2016
- Dec 2, 2016 We're Writing a Book! Dec 2, 2016
- Nov 10, 2016 Chrome Extension: Load in ServiceNow Frame Nov 10, 2016
- Sep 7, 2016 Force-Include Any Record Into an Update Set Sep 7, 2016
- Sep 1, 2016 GlideRecord Pagination - Page through your GlideRecord query Sep 1, 2016
- Jul 17, 2016 Granting Temporary Roles/Groups in ServiceNow Jul 17, 2016
- Jul 15, 2016 Scripted REST APIs & Retrieving RITM Variables via SRAPI Jul 15, 2016
- May 17, 2016 What's New in Helsinki? May 17, 2016
- Apr 27, 2016 Customizing UI16 Through CSS and System Properties Apr 27, 2016
- Apr 5, 2016 ServiceNow Versions: Express Vs. Enterprise Apr 5, 2016
- Mar 28, 2016 Update Set Collision Avoidance Tool: V2 Mar 28, 2016
- Mar 18, 2016 ServiceNow: What's New in Geneva & UI16 (Pt. 2) Mar 18, 2016
- Feb 22, 2016 Reference Field Auto-Complete Attributes Feb 22, 2016
- Feb 6, 2016 GlideRecord & GlideAjax: Client-Side Vs. Server-Side Feb 6, 2016
- Feb 1, 2016 Make Your Log Entries Easier to Find Feb 1, 2016
- Jan 29, 2016 A Better, One-Click Approval Jan 29, 2016
- Jan 25, 2016 Quickly Move Changes Between Update Sets Jan 25, 2016
- Jan 20, 2016 Customize the Reference Icon Pop-up Jan 20, 2016
- Jan 7, 2016 ServiceNow: Geneva & UI16 - What's new Jan 7, 2016
- Jan 4, 2016 Detect/Prevent Update Set Conflicts Before They Happen Jan 4, 2016
-
2015
- Dec 28, 2015 SN101: Boolean logic and ServiceNow's Condition Builder Dec 28, 2015
- Dec 17, 2015 Locate any record in any table, by sys_id in ServiceNow Dec 17, 2015
- Dec 16, 2015 Detecting Duplicate Records with GlideAggregate Dec 16, 2015
- Dec 11, 2015 Array.indexOf() not working in ServiceNow - Solution! Dec 11, 2015
- Dec 2, 2015 Understanding Dynamic Filters & Checking a Record Against a Filter Using GlideFilter Dec 2, 2015
- Oct 20, 2015 Bookmarklet: Load the current page in the ServiceNow frame Oct 20, 2015
- Aug 27, 2015 Easily Clone One User's Access to Another User Aug 27, 2015