72

I have a table with >1M rows of data and 20+ columns.

Within my table (tableX) I have identified duplicate records (~80k) in one particular column (troubleColumn).

If possible I would like to retain the original table name and remove the duplicate records from my problematic column otherwise I could create a new table (tableXfinal) with the same schema but without the duplicates.

I am not proficient in SQL or any other programming language so please excuse my ignorance.

delete from Accidents.CleanedFilledCombined 
where Fixed_Accident_Index 
in(select Fixed_Accident_Index from Accidents.CleanedFilledCombined 
group by Fixed_Accident_Index 
having count(Fixed_Accident_Index) >1);
TheGoat
  • 2,587
  • 3
  • 25
  • 58

9 Answers9

90

You can remove duplicates by running a query that rewrites your table (you can use the same table as the destination, or you can create a new table, verify that it has what you want, and then copy it over the old table).

A query that should work is here:

SELECT *
FROM (
  SELECT
      *,
      ROW_NUMBER()
          OVER (PARTITION BY Fixed_Accident_Index)
          row_number
  FROM Accidents.CleanedFilledCombined
)
WHERE row_number = 1
Technetium
  • 5,902
  • 2
  • 43
  • 54
Jordan Tigani
  • 26,089
  • 4
  • 60
  • 63
55

UPDATE 2019: To de-duplicate rows on a single partition with a MERGE, see:


An alternative to Jordan's answer - this one scales better when having too many duplicates:

SELECT event.* FROM (
  SELECT ARRAY_AGG(
    t ORDER BY t.created_at DESC LIMIT 1
  )[OFFSET(0)]  event
  FROM `githubarchive.month.201706` t 
  # GROUP BY the id you are de-duplicating by
  GROUP BY actor.id
)

Or a shorter version (takes any row, instead of the newest one):

SELECT k.*
FROM (
  SELECT ARRAY_AGG(x LIMIT 1)[OFFSET(0)] k 
  FROM `fh-bigquery.reddit_comments.2017_01` x 
  GROUP BY id
)

To de-duplicate rows on an existing table:

CREATE OR REPLACE TABLE `deleting.deduplicating_table`
AS
# SELECT id FROM UNNEST([1,1,1,2,2]) id
SELECT k.*
FROM (
  SELECT ARRAY_AGG(row LIMIT 1)[OFFSET(0)] k 
  FROM `deleting.deduplicating_table` row
  GROUP BY id
)
Julio Betta
  • 2,275
  • 1
  • 25
  • 25
Felipe Hoffa
  • 54,922
  • 16
  • 151
  • 325
  • 1
    Hi Felipe, Very cool!As a matter of curiosity on this, how would you construct a standardSQL query (only) that used 'DELETE' DML on the source table instead or rewriting in order to remove duplicates? – Kurt Maile Jan 06 '18 at 14:55
  • 1
    Answer updated with a one step de-duplicating for an existing table – Felipe Hoffa Nov 20 '18 at 01:19
  • when I ran the shorter version, my query took to long to respond. – intotecho Jul 24 '19 at 03:19
  • @intotecho weird - longer version takes less time to execute? try posting your job ids on the bigquery issue tracker – Felipe Hoffa Jul 24 '19 at 03:21
  • 1
    Ah, I forgot to include the first line CREATE OR REPLACE TABLE `deleting.deduplicating_table`. That's why it didn't finish. – intotecho Jul 24 '19 at 23:14
  • For the shorter version, can `ARRAY_AGG(x LIMIT 1)[OFFSET(0)]` be replaced with `ANY_VALUE(x)` ? – Minhaz Kazi Aug 06 '21 at 09:10
  • Will array agg work faster than row num.. even if we don't have many duplicates.. but we have to run similar query thousands times a day in our spark jobs – vikrant rana Mar 31 '22 at 23:09
41

Not sure why nobody mentioned DISTINCT query.

Here is the way to clean duplicate rows:

CREATE OR REPLACE TABLE project.dataset.table
AS
SELECT DISTINCT * FROM project.dataset.table
Julio Betta
  • 2,275
  • 1
  • 25
  • 25
Semra
  • 2,787
  • 28
  • 26
  • This doesn't work if you have more than one column in your table (or perhaps I'm doing something wrong?) – Oriol Nieto May 31 '19 at 18:28
  • 2
    Definitely the easiest way to do what I was trying to do - thanks! Doesn't directly answer OP's question, but it answers why I landed here :) @OriolNieto - it works with all your columns. You can swap * for a list of specific columns if you want to verify how it works – ZaxR Jul 30 '19 at 15:52
  • 3
    This doesn't work if the existing table is partitioned. – Cameron Oct 11 '19 at 14:38
  • 5
    I think if you have a column that's a struct it won't work with *. That might be what @OriolNieto was seeing. – Krista Davis Jan 30 '20 at 04:38
  • 4
    or if we want to dedup rows that have the same id but different values in other columns i.e. updated_at – Hui Zheng Feb 17 '20 at 22:23
  • or if we want to dedup rows that don't have a unique id, but where the values in all columns are exactly the same. :) – Jérémy Mar 16 '21 at 13:50
  • for just partitioned, create temp, drop and rename: `CREATE TABLE project.dataset.table_partitioned PARTITION BY -- range_bucket(partition_id, generate_array(0, 3999, 1)) AS SELECT distinct * FROM project.dataset.table; DROP TABLE project.dataset.table; ALTER TABLE project.dataset.table_partitioned RENAME TO project.dataset.table;` – AJ AJ May 28 '23 at 09:42
7

If your schema doesn’t have any records - below variation of Jordan’s answer will work well enough with writing over same table or new one, etc.

SELECT <list of original fields>
FROM (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY Fixed_Accident_Index) AS pos,
  FROM Accidents.CleanedFilledCombined
)
WHERE pos = 1

In more generic case - with complex schema with records/netsed fields, etc. - above approach can be a challenge.

I would propose to try using Tabledata: insertAll API with rows[].insertId set to respective Fixed_Accident_Index for each row. In this case duplicate rows will be eliminated by BigQuery

Of course, this will involve some client side coding - so might be not relevant for this particular question. I havent tried this approach by myself either but feel it might be interesting to try :o)

Julio Betta
  • 2,275
  • 1
  • 25
  • 25
Mikhail Berlyant
  • 165,386
  • 8
  • 154
  • 230
  • 1
    Thanks Mikhail, you've saved my bacon on a few occasions now! – TheGoat Apr 20 '16 at 20:28
  • If you have nested / repeated fields, the query I mentioned should work, as long as you set the query option to allow large results and to prevent flattening. – Jordan Tigani Apr 21 '16 at 20:30
  • Instead of listing the original fields, if you are using Standard SQL you can use something like: SELECT * except(pos) FROM (...) WHERE pos = 1; – killachaos Jul 27 '17 at 21:02
  • Hi Guys, Just on this deduping topic, lets say we pick one SQL above that works, and we want to priodically call it (savedquery) to execute and then write the dedup dataset back to the same table (effectively overriding). Assume in this scenario its scheduled using something like airflow, but there is another process that loads new events regularily, is there a chance of missing data here if say for a large table the sql is running and new data arrives at the same time - then you are writing back results that might not have the new data in it? Is this possible? How best to avoid if so? thx – Kurt Maile Jan 06 '18 at 15:54
  • @AntsaR - great! glad it helped :o) – Mikhail Berlyant Jan 14 '20 at 18:22
3

If you have a large-size partitioned table, and only have duplicates in a certain partition range. You don't want to overscan nor process the whole table. use the MERGE SQL below with predicates on partition range:

-- WARNING: back up the table before this operation
-- FOR large size timestamp partitioned table 
-- -------------------------------------------
-- -- To de-duplicate rows of a given range of a partition table, using surrage_key as unique id
-- -------------------------------------------

DECLARE dt_start DEFAULT TIMESTAMP("2019-09-17T00:00:00", "America/Los_Angeles") ;
DECLARE dt_end DEFAULT TIMESTAMP("2019-09-22T00:00:00", "America/Los_Angeles");

MERGE INTO `gcp_project`.`data_set`.`the_table` AS INTERNAL_DEST
USING (
  SELECT k.*
  FROM (
    SELECT ARRAY_AGG(original_data LIMIT 1)[OFFSET(0)] k 
    FROM `gcp_project`.`data_set`.`the_table` AS original_data
    WHERE stamp BETWEEN dt_start AND dt_end
    GROUP BY surrogate_key
  )

) AS INTERNAL_SOURCE
ON FALSE

WHEN NOT MATCHED BY SOURCE
  AND INTERNAL_DEST.stamp BETWEEN dt_start AND dt_end -- remove all data in partiion range
    THEN DELETE

WHEN NOT MATCHED THEN INSERT ROW

credit: https://gist.github.com/hui-zheng/f7e972bcbe9cde0c6cb6318f7270b67a

Julio Betta
  • 2,275
  • 1
  • 25
  • 25
Hui Zheng
  • 2,394
  • 1
  • 14
  • 18
1

Easier answer, without a subselect

  SELECT
      *,
      ROW_NUMBER()
          OVER (PARTITION BY Fixed_Accident_Index)
          row_number
  FROM Accidents.CleanedFilledCombined
  WHERE TRUE
  QUALIFY row_number = 1

The Where True is neccesary because qualify needs a where, group by or having clause

elauser
  • 86
  • 1
  • 6
0

Felipe's answer is the best approach for most cases. Here is a more elegant way to accomplish the same:

CREATE OR REPLACE TABLE Accidents.CleanedFilledCombined
AS
SELECT 
  Fixed_Accident_Index, 
  ARRAY_AGG(x LIMIT 1)[SAFE_OFFSET(0)].* EXCEPT(Fixed_Accident_Index)
FROM Accidents.CleanedFilledCombined AS x
GROUP BY Fixed_Accident_Index;

To be safe, make sure you backup the original table before you run this ^^

I don't recommend to use ROW NUMBER() OVER() approach if possible since you may run into BigQuery memory limits and get unexpected errors.

Igor-S
  • 655
  • 8
  • 10
0
  1. Update BigQuery schema with new table column as bq_uuid making it NULLABLE and type STRING 

  2. Create duplicate rows by running same command 5 times for example

insert into beginner-290513.917834811114.messages (id, type, flow, updated_at) Values(19999,"hello", "inbound", '2021-06-08T12:09:03.693646')

  1. Check if duplicate entries exist 
 select * from beginner-290513.917834811114.messages where id = 19999

  2. Use generate uuid function to generate uuid corresponding to each message 
UPDATE beginner-290513.917834811114.messages SET bq_uuid = GENERATE_UUID() where id>0

  3. Clean duplicate entries


DELETE FROM beginner-290513.917834811114.messages WHERE bq_uuid IN (SELECT bq_uuid FROM (SELECT bq_uuid, ROW_NUMBER() OVER( PARTITION BY updated_at ORDER BY bq_uuid ) AS row_num FROM beginner-290513.917834811114.messages ) t WHERE t.row_num > 1 );

0

When it comes to large deduplication, the QUALIFY command appears to be the most effective and efficient option, as explained here

nadavw
  • 31
  • 3