Doppelte Datensätze löschen

Hallo,

um mir Programmierarbeit zu ersparen möchte ich eine Methode erstellen, die in der aktuellen Auswahl einer beliebigen Tabelle doppelte Datensätze findet und löscht. Beim Vergleich der Datensätze sollen alle Felder berücksichtigt werden bis auf die, deren Inhalt einmalig ist (z.B. der primary key).

Für einen effizienten Vergleich möchte ich die Auswahl der Tabelle nach all diesen Feldern sortieren. Hat eine Tabelle z.B. die Felder Feld1 - Feld3 bräuchte ich so etwas wie

order by([meineTabelle]Feld1;[meineTabelle]Feld2;[meineTabelle]Feld3)

aber generisch erzeugt. Mir fällt da im Moment nur ein die Sortierung über den Umweg “process 4d tags” zu machen, da ich hier meine Sortierkriterien als String zusammenbasteln kann. Vielleicht kennt jemand noch eine bessere Lösung z.B. mit “order by formula”?

Falls Interesse besteht, meine Methode sieht bisher folgendermaßen aus:

<code 4D>

// ----------------------------------------------------
// Method: deleteDuplicates
// Description
//
//
// Parameters
// ----------------------------------------------------

C_POINTER($1)
C_POINTER($p_myTable;$p_curField)
ARRAY POINTER($myFields;0)
ARRAY LONGINT($myFieldTypes;0)

C_LONGINT($fieldType;$fieldLength;$tableNum)
C_BOOLEAN($hasIndex;$isUnique)

C_TEXT($orderExp)

$p_myTable:=$1
$tableNum:=Table($p_myTable)
$tableName:=Table name($tableNum)
$lastFieldNum:=Get last field number($p_myTable)

ARRAY LONGINT($fieldTypesToTest;0)
APPEND TO ARRAY($fieldTypesToTest;Is alpha field)
APPEND TO ARRAY($fieldTypesToTest;Is text)
APPEND TO ARRAY($fieldTypesToTest;Is Boolean)
APPEND TO ARRAY($fieldTypesToTest;Is date)
APPEND TO ARRAY($fieldTypesToTest;Is float)
APPEND TO ARRAY($fieldTypesToTest;Is integer)
APPEND TO ARRAY($fieldTypesToTest;Is longint)
APPEND TO ARRAY($fieldTypesToTest;Is real)
APPEND TO ARRAY($fieldTypesToTest;Is time)

_o_C_INTEGER($numTxtFields;$numBoolFields;$numDateFields;$numRealFields;$numIntFields;$numTimeFields)

ARRAY INTEGER($myFieldPos;0)

//for all fields we take int account: store pointer and type in arrays.
// store field positions separated by type: (for example one valid field is the third integer field) in array $myFieldPos
For ($i;1;$lastFieldNum)
If (Is field number valid($p_myTable;$i)=True)
$p_curField:=Field($tableNum;$i)
GET FIELD PROPERTIES($p_curField;$fieldType;$fieldLength;$hasIndex;$isUnique)
If ($isUnique=False) & (Find in array($fieldTypesToTest;$fieldType)>0)
APPEND TO ARRAY($myFields;$p_curField)
APPEND TO ARRAY($myFieldTypes;$fieldType)
$orderExp:=$orderExp+Field name($p_curField)+";"
Case of
: ($fieldType=Is alpha field) | ($fieldType=Is text)
$numTxtFields:=$numTxtFields+1
APPEND TO ARRAY($myFieldPos;$numTxtFields)
: ($fieldType=Is Boolean)
$numBoolFields:=$numBoolFields+1
APPEND TO ARRAY($myFieldPos;$numBoolFields)
: ($fieldType=Is date)
$numDateFields:=$numDateFields+1
APPEND TO ARRAY($myFieldPos;$numDateFields)
: ($fieldType=Is real)
$numRealFields:=$numRealFields+1
APPEND TO ARRAY($myFieldPos;$numRealFields)
: ($fieldType=Is integer) | ($fieldType=Is longint)
$numIntFields:=$numIntFields+1
APPEND TO ARRAY($myFieldPos;$numIntFields)
: ($fieldType=Is time)
$numTimeFields:=$numTimeFields+1
APPEND TO ARRAY($myFieldPos;$numTimeFields)
End case
End if
End if
End for

//allocate memory to store values of one record in arrays, needed to store the values of the previous record
ARRAY TEXT($arPrevTxtValues;$numTxtFields)
ARRAY BOOLEAN($arPrevBoolValues;$numBoolFields)
ARRAY DATE($arPrevDateValues;$numDateFields)
ARRAY REAL($arPrevRealValues;$numRealFields)
ARRAY LONGINT($arPrevIntValues;$numIntFields)
ARRAY TIME($arPrevTimeValues;$numTimeFields)

ORDER BY FORMULA($p_myTable->;$orderExp) //wrong --> order by formula does not work this way

//store each field in an array element, each field has its own array position (stored in array $myFieldPos)
For ($k;1;Size of array($myFields))
Case of
: ($myFieldTypes{$k}=Is alpha field) | ($myFieldTypes{$k}=Is text)
$arPrevTxtValues{$myFieldPos{$k}}:=$myFields{$k}->
: ($myFieldTypes{$k}=Is Boolean)
$arPrevBoolValues{$myFieldPos{$k}}:=$myFields{$k}->
: ($myFieldTypes{$k}=Is date)
$arPrevDateValues{$myFieldPos{$k}}:=$myFields{$k}->
: ($myFieldTypes{$k}=Is real)
$arPrevRealValues{$myFieldPos{$k}}:=$myFields{$k}->
: ($myFieldTypes{$k}=Is integer) | ($myFieldTypes{$k}=Is longint)
$arPrevIntValues{$myFieldPos{$k}}:=$myFields{$k}->
: ($myFieldTypes{$k}=Is time)
$arPrevTimeValues{$myFieldPos{$k}}:=$myFields{$k}->
End case
End for
ARRAY LONGINT($ar_recNums;0)
SELECTION TO ARRAY($p_myTable->;$ar_recNums)
For ($i;2;Size of array($ar_recNums))
GOTO RECORD($p_myTable->;$ar_recNums{$i})
$recsAreEqual:=True

  //do the comparison with the values of the previous record
For ($k;1;Size of array($myFields))
	Case of 
		: ($myFieldTypes{$k}=Is alpha field) | ($myFieldTypes{$k}=Is text)
			If ($arPrevTxtValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevTxtValues{$myFieldPos{$k}}:=$myFields{$k}->
		: ($myFieldTypes{$k}=Is Boolean)
			If ($arPrevBoolValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevBoolValues{$myFieldPos{$k}}:=$myFields{$k}->
		: ($myFieldTypes{$k}=Is date)
			If ($arPrevDateValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevDateValues{$myFieldPos{$k}}:=$myFields{$k}->
		: ($myFieldTypes{$k}=Is real)
			If ($arPrevRealValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevRealValues{$myFieldPos{$k}}:=$myFields{$k}->
		: ($myFieldTypes{$k}=Is integer) | ($myFieldTypes{$k}=Is longint)
			If ($arPrevIntValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevIntValues{$myFieldPos{$k}}:=$myFields{$k}->
		: ($myFieldTypes{$k}=Is time)
			If ($arPrevTimeValues{$myFieldPos{$k}}#$myFields{$k}->)
				$recsAreEqual:=False
			End if 
			$arPrevTimeValues{$myFieldPos{$k}}:=$myFields{$k}->
	End case 
End for 

If ($recsAreEqual=True)
	DELETE RECORD($p_myTable->)
End if 

End for

</code 4D>

Das ganze kann man auch leichter mit C_Object realisieren. Im Object werden dann die Werte des vorangegangenen Datensatzes gespeichert. Ohne Object funktioniert es nach einem flüchtigen Test aber schneller.

Dass Ihre Methode nicht funktioniert, haben Sie ja selbst geschrieben.

Der einfachste Weg, doppelte Datensätze zu löschen, ist aus meiner Sicht, jeden mit jedem zu vergleichen:

<code 4D>
ALL RECORDS([MY_TABLE])
$n:=Records in selection([MY_TABLE])
CREATE EMPTY SET([MY_TABLE];“SET”)
For ($i;1;$n-1)
For ($k;$i+1;$n)
If (RECORDS_GLEICH($i;$k))
ADD TO SET([MY_TABLE];“SET”)
End if
End for
End for
USE SET(“SET”)
DELETE SELECTION([MY_TABLE])

</code 4D>

Wobei die Methode RECORDS_GLEICH jeweils die zwei Datensätze mit GOTO SELECTED RECORD öffnet, jeweils alle Feldinhalte in ein Array schreibt und dann beide Arrays vergleicht.

Bei bis zu 1000 Datensätzen dürfte das einigermaßen schnell funktionieren, danach wird’s zäh.
Da hilft dann nur vorsortieren nach einem Feld (oder nach einem Hash aus allen Feldern) und dann nur die Datensätze vergleichen, bei denen das Feld/der Hash übereinstimmt.

Das ist eine Lösung, aber wie Sie ja schon schreiben ziemlich rechenaufwändig. Bei meiner Methode fehlt ja nur noch die Sortierung. Die ist auch noch hinzubekommen. Wenn’s gar nicht anders geht halt den order by-Befehl zur Laufzeit als String erzeugen und mit process 4d tags ausführen.

Danke für Ihre Antwort!

Dann sollten Sie das probieren.
Ich befürchte aber, dass eine Sortierung nach hunderten von Feldern nicht besonders schnell ist.
Wahrscheinlich langsamer, als die Hash-Methode.
Aber das ist nur Spekulation, ich kenne mich mit PROCESS 4D TAGS nicht besonders aus.