Array Cleanup

Concepts covered: Array Operations

Listing 102 counts 3 amenities but offers 2, and the NULL slots on 103 and 104 turn every search on those rows into NULL. Both defects trace back to collection: a host taps the same checkbox twice, and an unused slot is stored as NULL. Cleaning the list once means every function after it works on real, unique values. The examples here name the uncleaned column raw_amenities.

array_distinct removes repeats

array_distinct(raw_amenities) keeps the first occurrence of each value and drops later copies, so the survivors stay in their original order. NULL counts as one more value: a single NULL survives, and an array holding 3 NULLs keeps 1. cardinality counts every element, NULLs included, which is why listing 103 still reports 3 after deduplication.

WITH raw AS (
  SELECT
    ARRAY[amenity_1, amenity_2, amenity_3] AS raw_amenities,
    listing_id
  FROM listing_amenities
)
SELECT
  listing_id,
  cardinality(raw_amenities) AS raw_count,
  array_distinct(raw_amenities) AS deduped,
  cardinality(array_distinct(raw_amenities)) AS deduped_count
FROM raw
listing_amenities
listing_idcityamenity_1amenity_2amenity_3
101Austinwifihot_tubparking
102Denverparkingwifiwifi
103AustinkitchenNULLparking
104Denverhot_tubkitchenNULL
105Austinwifikitchenhot_tub
Result
listing_idraw_countdedupeddeduped_count
1013["wifi","hot_tub","parking"]3
1023["parking","wifi"]2
1033["kitchen",null,"parking"]3
1043["hot_tub","kitchen",null]3
1053["wifi","kitchen","hot_tub"]3

Removing NULLs: the call that backfires

The tempting call is array_remove(raw_amenities, NULL), and it returns NULL for every row: array_remove hands back NULL whenever its second argument is NULL, before it looks at the array. Even with that rule lifted, it removes elements equal to the argument, and NULL = NULL is never true.

The call that works is array_except(raw_amenities, ARRAY[CAST(NULL AS VARCHAR)]). array_except(x, y) returns the elements of x that are absent from y, and as a set operation it lets a NULL in x match the NULL in y. The CAST gives the 1-element array the VARCHAR type the amenities have.

WITH raw AS (
  SELECT
    ARRAY[amenity_1, amenity_2, amenity_3] AS raw_amenities,
    listing_id
  FROM listing_amenities
)
SELECT
  listing_id,
  array_remove(raw_amenities, NULL) AS removed,
  array_except(raw_amenities, ARRAY[CAST(NULL AS VARCHAR)]) AS excepted
FROM raw
listing_idremovedexcepted
101NULL[wifi, hot_tub, parking]
102NULL[parking, wifi]
103NULL[kitchen, parking]
104NULL[hot_tub, kitchen]
105NULL[wifi, kitchen, hot_tub]

Listing 102 in the excepted column reads [parking, wifi]. Set functions always return distinct elements, so array_except dropped the second wifi along with the NULLs. For amenities that is the goal, since a listing either has wifi or lacks it. For sequences where repeats carry information, such as the pages a guest viewed in order, the same call erases data; a later lesson covers filtering that keeps repeats.

call on the raw arraylisting 102listing 103
array_distinct(x)[parking, wifi][kitchen, NULL, parking]
array_remove(x, NULL)NULLNULL
array_except(x, ARRAY[NULL])[parking, wifi][kitchen, parking]

The set functions, all deduplicating

Clean once, then search

With NULLs gone, contains sees every element and returns false on a miss. The CTE below cleans in one place, and the no-wifi filter, written here as a comparison with false, now returns listings 103 and 104 with no COALESCE.

WITH clean AS (
  SELECT
    array_except(ARRAY[amenity_1, amenity_2, amenity_3], ARRAY[CAST(NULL AS VARCHAR)]) AS amenities,
    listing_id
  FROM listing_amenities
)
SELECT
  listing_id,
  amenities
FROM clean
WHERE contains(amenities, 'wifi') = false
listing_idamenities
103[kitchen, parking]
104[hot_tub, kitchen]

The gap between raw and clean sizes doubles as a data-quality measure. Summed across listings, it counts the junk slots a cleanup step removed, a figure worth tracking as new hosts arrive.

WITH raw AS (
  SELECT
    ARRAY[amenity_1, amenity_2, amenity_3] AS raw_amenities,
    listing_id
  FROM listing_amenities
)
SELECT
  COUNT(*) AS listings,
  SUM(cardinality(raw_amenities) - cardinality(array_except(raw_amenities, ARRAY[CAST(NULL AS VARCHAR)]))) AS junk_slots
FROM raw
listingsjunk_slots
53

The 3 junk slots are the second wifi on listing 102 and the NULL slots on 103 and 104. The raw arrays hold 15 elements and the cleaned ones 12, so 1 slot in 5 was junk. A jump in that ratio after a release points at the form that collects amenities.

Do
  • Clean arrays in one CTE and point every later step at the clean column
  • Drop NULLs from set-like lists with array_except and a 1-element NULL array
  • Compare cardinality before and after cleanup to measure how much changed
Avoid
  • Don't pass NULL to array_remove; it returns a NULL array
  • Don't deduplicate sequences where repeats carry meaning, like page views
  • Don't search arrays with contains while NULL slots remain in them

Listing 107's raw array is [pool, NULL, pool, NULL]. What do the 2 cleanup columns return for it?

SELECT
  array_distinct(raw_amenities) AS deduped,
  array_except(raw_amenities, ARRAY[CAST(NULL AS VARCHAR)]) AS cleaned
FROM raw
WHERE listing_id = 107

About this lesson

This section is part of the Complex Data: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.