システムにデータが入ったら、悪いデータベース設計を修復する
-
09-06-2019 - |
質問
それが質問ではないことはわかっています...えーっと、とにかくここが質問です。
私は、次のような 1 つのテーブルを持つデータベースを継承しました。その目的は、さまざまな (200 の奇妙な) 国でどのような種が発見されているかを記録することです。
ID
Species
Afghanistan
Albania
Algeria
American Samoa
Andorra
Angola
....
Western Sahara
Yemen
Zambia
Zimbabwe
データのサンプルは次のようになります
id Species Afghanistan Albania American Samoa
1 SP1 null null null
2 SP2 1 1 null
3 SP3 null null 1
これは典型的な多対多の状況のようで、3 つのテーブルが必要です。種、国、および種が見つかった国
リンク テーブル (SpeciesFoundIn Country) には、種テーブルと Country テーブルの両方に外部キーがあります。
(図を書くのは難しいです!)
Species
SpeciesID SpeciesName
Country
CountryID CountryName
SpeciesFoundInCountry
CountryID SpeciesID
元のメガ テーブルに 1 がある列名と SpeciesID に基づいて、新しい Country テーブルから CountryID を取得する挿入ステートメントを生成できる魔法の方法はありますか?
1 つの国に対して行うことができます (これは、私が望んでいることを示すための選択です)
SELECT Species.ID, Country.CountryID
FROM Country, Species
WHERE (((Species.Afghanistan)=1)) AND (((Country.Country)="Afghanistan"));
(メガテーブルは種と呼ばれます)
ただし、この戦略を使用すると、元のテーブルの各列に対してクエリを実行する必要があります。
SQLでこれを行う方法はありますか?
大量の where 句を OR 演算して SQL を作成するスクリプトを作成できると思いますが、洗練されていないように思えます。
何かご意見(または説明が必要ですか)はありますか?
解決
これは 1 回限りのインポート プロセスであるため、スクリプトを使用してすべての個別のクエリを生成します。
Excel などの一部のプログラムは、異なる次元のデータを混合する (列名と行内のデータを比較する) ことに優れていますが、リレーショナル データベースはほとんどそうではありません。
ただし、一部のシステム (驚くべきことに、Microsoft Access など) には、データを正規化するために使用できる便利なツールがある場合があります。個人的には、スクリプトを書いたほうが早いと思いますが、Access とスクリプトに関するあなたの相対的なスキルは私とは異なる可能性があります。
他のヒント
なぜ SQL でそれを行うのでしょうか?変換を行う小さなスクリプトを作成するだけです。
これらに遭遇した場合は、SQL で変換を実行するのではなく、変換を実行するスクリプトを作成します。通常、私にとってはその方がはるかに速くて簡単です。使いやすい言語を選択してください。
これが SQL Server の場合は、Unpivot コマンドを使用しますが、割り当てたタグを見ると、それはアクセス用です。そうですか?
がありますが、 Access でのピボット コマンド, 、逆のステートメントはありません。
複雑な結合で実現できるようです。これをチェックして 興味深い記事 選択コマンドでピボットを解除する方法について詳しく説明します。
おそらく、適切な場所に置換テーブルを作成することになるでしょう。スクリプトは使用できるスクリプト言語によって異なりますが、現在あるテーブルの列をリストするだけで国 ID テーブルを作成できるはずです。それが完了したら、いくつかの文字列置換を実行して、一意の国名をすべて調べて、指定された国の列が null ではない SpeciesFoundIn Country テーブルに挿入できます。
おそらく、賢くシステム テーブルに列名を問い合わせて、動的クエリ文字列を作成して実行することもできますが、正直に言って、それは SQL ステートメントを生成する簡単なスクリプトよりも醜いでしょう。
コードベースに埋め込まれた古いテーブルにアクセスする動的 SQL コードが多すぎないことを願っています。それは可能性があります 本当に 難しい部分。
SQL Server では、これにより、デモするカスタム選択が生成されます。挿入を推定することができます
select 'SELECT Species.ID, Country.CountryID FROM Country, Species WHERE (((Species.' + c.name + ')=1)) AND (((Country.Country)="' + c.name + '"))' from syscolumns c inner join sysobjects o on o.id = c.id where o.name = 'old_table_name'
他のものと同様に、私はおそらく、自分に合った方法で一度だけ簡単な修正を行うと思います。
このような種類の変換では、1 回限りの項目であり、簡単な修正であり、コードがエレガントである必要はなく、機能するだけで十分です。このようなタイプのことについて、私はさまざまな方法でそれを行ってきました。
SQL Server の場合は、sys.columns テーブルを使用して、元のテーブルのすべての列を検索できます。その後、動的 SQL と pivot コマンドを使用して、必要なことを実行できます。構文についてはオンラインで調べてください。
すべての列にクエリを含む SQL を生成するための小さなスクリプトを作成するというあなたの提案に、私は間違いなく同意します。
実際、この魔法のクエリを考えるのに費やした時間内に、スクリプトはすでに完成している可能性があります (1 回だけ使用して、その後捨てることになるので、すべてを魔法のように完璧にすることに何の意味がありますか)
申し訳ありませんが、血まみれの投稿パーサーにより、私の投稿の空白と書式設定が削除されました。ログが読みにくくなります。
@ストンプ:
答えを入力するボックスの上には、いくつかのボタンがあります。101010 のものはコードサンプルです。コードであるテキストをすべて選択し、そのボタンをクリックします。そうすると、あまりいじられなくなります。
cout>>"I don't know C"
cout>>"Hello World"
非常に大まかに言うと、Union クエリを使用します。
Dim db As Database
Dim tdf As TableDef
Set db = CurrentDb
Set tdf = db.TableDefs("SO")
strSQL = "SELECT ID, Species, """ & tdf.Fields(2).Name _
& """ AS Country, [" & tdf.Fields(2).Name & "] AS CountryValue FROM SO "
For i = 3 To tdf.Fields.Count - 1
strSQL = strSQL & vbCrLf & "UNION SELECT ID, Species, """ & tdf.Fields(i).Name _
& """ AS Country, [" & tdf.Fields(i).Name & "] AS CountryValue FROM SO "
Next
db.CreateQueryDef "UnionSO", strSQL
これで、新しいデザインに追加できるビューが作成されます。
「ひどい BAD データベース設計」というタイトルを読んだとき、それがどのくらいひどいのか知りたくなりました。あなたは私を失望させませんでした:)
他の人が述べたように、スクリプトを使用するのが最も簡単な方法です。これは、PHP で約 15 行のコードを記述することで実現できます。
SELECT * FROM ugly_table;
while(row)
foreach(row as field => value)
if(value == 1)
SELECT country_id from country_table WHERE country_name = field;
if(field == 'Species')
SELECT species_id from species_table WHERE species_name = value;
INSERT INTO better_table (...)
明らかに、これは疑似コードであり、そのままでは動作しません。ここに挿入ステートメントを追加することで、国と種のテーブルにその場でデータを入力することもできます。
申し訳ありませんが、私は Access プログラミングをほとんどやったことがありませんが、役立つはずのガイダンスを提供できます。
まず問題を見ていきましょう。通常、元のテーブルの行ごとに SpeciesFoundIn Country に複数の行を生成する必要があると想定されます。言い換えれば、種は複数の国に存在する傾向があります。これは実際には、デカルト積、つまり結合基準のない結合を使用すると簡単に実行できます。
デカルト積を実行するには、 Country テーブルを作成する必要があります。テーブルには、1 から N までの country_id (N は一意の国の数、200 程度) と国名が含まれている必要があります。作業を簡単にするには、列の順序で 1 から N までの数字を使用するだけです。そうなるとアフガニスタンが1位、アルバニアが2位になります...ジンバブエ N.これを行うにはシステム テーブルを使用できるはずです。
次に、種と国ごとに 0 または 1 を含む文字列を含むテーブルまたはビューを元のテーブルから作成します。null (null ではない) をテキスト 0 または 1 に変換し、すべての値を 1 つの文字列に連結する必要があります。テーブルの説明と正規表現を使用したテキスト エディターを使用すると、これが簡単になります。最初に単一の列で実験し、それが機能したら、すべての列でビューの作成/挿入を編集します。
次に、結合基準を指定せずに 2 つのテーブルを結合します。これにより、すべての国のすべての種の記録が得られます。もうすぐそこに到達します。
ここで必要なのは、無効なレコードをフィルターで除外することだけです。無効なレコードには、文字列内の対応する位置にゼロが含まれます。country テーブルの country_code 列には部分文字列の位置が含まれているため、必要なのは、それが 0 であるレコードをフィルターで除外することだけです。
where substring(new_column,country_code) = '1'
種テーブルを作成してそれに結合する必要があります。
where a.species_name = b.species_name
a と b はテーブルの別名です。
これがお役に立てば幸いです
ところで、
古いテーブルに対してすでに実行されているクエリがある場合は、新しいテーブルを使用して古いテーブルを複製するビューを作成する必要があります。テーブルを非正規化するには、group by を実行する必要があります。
古いテーブル/ビューは今後サポートされなくなり、すべての新しいクエリまたは古いクエリの更新では新しいテーブルを使用する必要があることをユーザーに伝えます。
似たような SQL ステートメントを大量に作成してすべて実行する必要がある場合、Excel が非常に便利であることがよくあります。元のクエリを取得します。列 A に国のリストがあり、列 B に SQL ステートメントがあり、SQL で国が表示される場所にセル参照が挿入されたテキスト (引用符で囲まれた) としてフォーマットされている場合
例えば="INSERT INTO new_table SELECT ...(種。" & A1 & ")= ...));"
次に、数式をコピーして 200 の異なる SQL ステートメントを作成し、その列をエディターにコピー/ペーストして F5 キーを押します。もちろん、必要なだけ変数を使用してこれを行うことができます。
同様の問題に直面したとき、SQL スクリプトを生成するスクリプトを生成すると便利であることがわかりました。これは、アフガニスタンの代わりに %PAR1% を使用するために抽象化された、あなたが指定したサンプルです。
SELECT Species.ID, Country.CountryID
FROM Country, Species
WHERE (((Species.%PAR1%)=1)) AND (((Country.Country)="%PAR1%"))
UNION
また、すべての選択を結合する方法として、キーワード Union が追加されました。
次に、既存のデータから生成された国のリストが必要です。
アフガニスタンアルバニア。、。
次に、カントリーリストを繰り返すことができるスクリプトが必要であり、反復ごとに、最初の反復で%PAR1%をアフガニスタン、2回目の反復などに置き換える出力を生成します。このアルゴリズムは、ワード プロセッサの差し込み印刷に似ています。このスクリプトを書くのは少し大変です。しかし、一度入手すれば、今回のような何十もの 1 回限りのプロジェクトで使用できます。
最後に、最後の「UNION」を手動でセミコロンに戻す必要があります。
Access にこの巨大なユニオンを実行させることができれば、必要なデータを必要な形式で取得して、新しいテーブルに挿入できます。
SpeciesFoundIn Country テーブルに一時的な変更を加えて、3 段階のプロセスにします。そのテーブルに列を追加して、国名を保存します。その場合、手順は次のようになります。
1) ソーステーブル内の列を調べ、true 値を持つ各列のレコードを SpeciesFoundIn Country に作成するスクリプトを作成/実行します。このレコードには国名が含まれます。2) Country Name の Country テーブルに結合して SpeciesFoundIn Country. CountryID フィールドを更新する SQL ステートメントを実行します。3) CountryName 列を削除して、SpeciesFoundIn Country テーブルをクリーンアップします。
ここに要点を示すための小さな MS Access VB/VBA 疑似コードを示します。
Public Sub CreateRelationshipRecords()
Dim rstSource as DAO.Recordset
Dim rstDestination as DAO.Recordset
Dim fld as DAO.Field
dim strSQL as String
Dim lngSpeciesID as Long
strSQL = "SELECT * FROM [ORIGINALTABLE]"
Set rstSource = CurrentDB.OpenRecordset(strSQL)
set rstDestination = CurrentDB.OpenRecordset("SpeciesFoundInCountry")
rstSource.MoveFirst
' Step through each record in the original table
Do Until rstSource.EOF
lngSpeciesID = rstSource.ID
' Now step through the fields(columns). If the field
' value is one (1), then create a relationship record
' using the field name as the Country Name
For Each fld in rstSource.Fields
If fld.Value = 1 then
with rstDestination
.AddNew
.Fields("CountryID").Value = Null
.Fields("CountryName").Value = fld.Name
.Fields("SpeciesID").Value = lngSpeciesID
.Update
End With
End IF
Next fld
rstSource.MoveNext
Loop
' Clean up
rstSource.Close
Set rstSource = nothing
....
End Sub
この後、単純な SQL ステートメントを実行して、SpeciesFoundIn Country テーブル内の CountryID 値を更新できます。
UPDATE SpeciesFoundIn Country INNER JOIN Country ON SpeciesFoundIn Country. CountryName = Country. CountryName SET SpeciesFoundIn Country. CountryID = Country. CountryID;
最後に、 CountryName 列を削除して SpeciesFoundIn Country テーブルをクリーンアップするだけです。
****サイドノート:ISO の略語 (国コード) も含まれる国別表があると便利であることがわかりました。場合によっては、 Country テーブルへの結合をクエリに含める必要がないように、他のテーブルの外部キーとして使用されることがあります。
これは (できれば) 1 回限りの演習であるため、洗練されていないソリューションでも思ったほど悪くないかもしれません。
問題は (あなたも十分に気づいていると思いますが)、クエリのある時点でこれらの列をすべてリストする必要があることです。:( 問題は、これを行う最もエレガントな方法は何でしょうか?以下は私の試みです。非常に多くの列があるため扱いにくく見えますが、それがあなたが求めているものであるか、少なくとも正しい方向を示している可能性があります。
考えられる SQL ソリューション:
/* if you have N countries */
CREATE TABLE Country
(id int,
name varchar(50))
INSERT Country
SELECT 1, 'Afghanistan'
UNION SELECT 2, 'Albania',
UNION SELECT 3, 'Algeria' ,
UNION SELECT 4, 'American Samoa' ,
UNION SELECT 5, 'Andorra' ,
UNION SELECT 6, 'Angola' ,
...
UNION SELECT N-3, 'Western Sahara',
UNION SELECT N-2, 'Yemen',
UNION SELECT N-1, 'Zambia',
UNION SELECT N, 'Zimbabwe',
CREATE TABLE #tmp
(key varchar(N),
country_id int)
/* "key" field needs to be as long as N */
INSERT #tmp
SELECT '1________ ... _', 'Afghanistan'
/* '1' followed by underscores to make the length = N */
UNION SELECT '_1_______ ... ___', 'Albania'
UNION SELECT '__1______ ... ___', 'Algeria'
...
UNION SELECT '________ ... _1_', 'Zambia'
UNION SELECT '________ ... __1', 'Zimbabwe'
CREATE TABLE new_table
(country_id int,
species_id int)
INSERT new_table
SELECT species.id, country_id
FROM species s ,
#tmp t
WHERE isnull( s.Afghanistan, ' ' ) +
isnull( s.Albania, ' ' ) +
... +
isnull( s.Zambia, ' ' ) +
isnull( s.Zimbabwe, ' ' ) like t.key
私のおすすめ
個人的には、こんなことはしません。私なら、国 ID をハードコーディングすることを除いて、あなたがほのめかしたような手っ取り早く汚い解決策を実行します (これは 1 回しか実行しないからですよね?)国テーブルを作成した直後にこれを実行できるため、すべての ID が何であるかがわかります。
INSERT new_table SELECT Species.ID, 1 FROM Species WHERE Species.Afghanistan = 1
INSERT new_table SELECT Species.ID, 2 FROM Species WHERE Species.Albania= 1
...
INSERT new_table SELECT Species.ID, 999 FROM Species WHERE Species.Zambia= 1
INSERT new_table SELECT Species.ID, 1000 FROM Species WHERE Species.Zimbabwe= 1