SQLite에없는 경우 ALTER TABLE ADD COLUMN


89

최근에 기존 SQLite 데이터베이스 테이블 몇 개에 열을 추가해야했습니다. 이것은 ALTER TABLE ADD COLUMN. 물론 테이블이 이미 변경된 경우에는 그대로 두어야합니다. 불행하게도, SQLite는은 지원하지 않습니다 IF NOT EXISTS에 절을 ALTER TABLE.

현재 해결 방법은 이 Python 예제 와 마찬가지로 ALTER TABLE 문을 실행하고 "중복 된 열 이름"오류를 무시하는 것입니다 (그러나 C ++에서).

그러나 데이터베이스 스키마를 설정하는 일반적인 접근 방식은 또는 명령 줄 도구를 사용하여 실행할 수있는 CREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS문을 포함하는 .sql 스크립트를 사용 sqlite3_exec하는 것 sqlite3입니다. ALTER TABLE이 스크립트 파일을 넣을 수 없습니다. 그 문이 실패하면 그 이후의 어떤 것도 실행되지 않기 때문입니다.

테이블 정의를 한곳에두고 .sql과 .cpp 파일로 나누지 않고 싶습니다. ALTER TABLE ADD COLUMN IF NOT EXISTS순수한 SQLite SQL에서 해결 방법을 작성하는 방법이 있습니까?

답변:


64

99 % 순수한 SQL 방법이 있습니다. 아이디어는 스키마를 버전 화하는 것입니다. 두 가지 방법으로이 작업을 수행 할 수 있습니다.

  • 'user_version'pragma 명령 ( PRAGMA user_version)을 사용하여 데이터베이스 스키마 버전에 대한 증분 번호를 저장합니다.

  • 고유 한 정의 된 테이블에 버전 번호를 저장하십시오.

이러한 방식으로 소프트웨어가 시작되면 데이터베이스 스키마를 확인하고 필요한 경우 ALTER TABLE쿼리를 실행 한 다음 저장된 버전을 증가시킬 수 있습니다. 특히 데이터베이스가 수년에 걸쳐 몇 번 증가하고 변경되는 경우 다양한 업데이트를 "블라인드"하는 것보다 훨씬 낫습니다.


7
의 초기 값은 user_version무엇입니까? 나는 0이라고 가정하지만 그것이 문서화되어 있으면 좋을 것입니다.
Craig McQueen 2014 년

심지어이와 함께, SQLite는 지원하지 않기 때문에 그것은 순수한 SQL에서 수행 할 수 있습니다 IF와는 ALTER TABLE조건이없는? "99 % 순수 SQL"이란 무엇을 의미합니까?
크레이그 맥퀸

1
@CraigMcQueen의 초기 값은 user_version0으로 보이지만 실제로는 사용자가 정의한 값이므로 자신 만의 초기 값을 만들 수 있습니다.
MPelletier 2014 년

7
user_version초기 값 에 대한 질문은 기존 데이터베이스가 있고 user_version이전에 사용한 적이 없지만 사용 을 시작하려는 경우 관련이 있으므로 sqlite가 특정 초기 값으로 설정한다고 가정해야합니다.
Craig McQueen 2014 년

1
@CraigMcQueen 동의하지만 문서화되지 않은 것 같습니다.
MPelletier 2014 년

30

한 가지 해결 방법은 열을 생성하고 열이 이미 존재하는 경우 발생하는 예외 / 오류를 포착하는 것입니다. 여러 열을 추가 할 때 하나의 중복으로 인해 다른 열이 생성되는 것을 막지 않도록 별도의 ALTER TABLE 문에 추가합니다.

sqlite가-NET , 우리는 다음과 같이했다. 중복 된 sqlite 오류를 다른 sqlite 오류와 구별 할 수 없기 때문에 완벽하지 않습니다.

Dictionary<string, string> columnNameToAddColumnSql = new Dictionary<string, string>
{
    {
        "Column1",
        "ALTER TABLE MyTable ADD COLUMN Column1 INTEGER"
    },
    {
        "Column2",
        "ALTER TABLE MyTable ADD COLUMN Column2 TEXT"
    }
};

foreach (var pair in columnNameToAddColumnSql)
{
    string columnName = pair.Key;
    string sql = pair.Value;

    try
    {
        this.DB.ExecuteNonQuery(sql);
    }
    catch (System.Data.SQLite.SQLiteException e)
    {
        _log.Warn(e, string.Format("Failed to create column [{0}]. Most likely it already exists, which is fine.", columnName));
    }
}

28

SQLite는 또한 "table_info"라는 pragma 문을 지원합니다.이 구문은 열 이름 (및 열에 대한 기타 정보)이있는 테이블의 열당 한 행을 반환합니다. 쿼리에서 이것을 사용하여 누락 된 열을 확인하고 존재하지 않는 경우 테이블을 변경할 수 있습니다.

PRAGMA table_info(foo_table_name)

http://www.sqlite.org/pragma.html#pragma_table_info


30
링크 대신 검색을 완료하는 데 사용할 코드를 제공하면 답변이 훨씬 더 우수 할 것입니다.
Michael Alan Huff 2014

PRAGMA table_info (테이블 _ 이름). 이 명령은 table_name의 각 열을 결과의 행으로 나열합니다. 이 결과를 기반으로 컬럼이 존재하는지 여부를 판별 할 수 있습니다.
Hao Nguyen

2
더 큰 SQL 문에서 pragma를 결합하여 열이 존재하지 않는 경우 단일 쿼리에만 추가되도록하는 방법이 있습니까?
Michael

1
@남자 이름. 내가 아는 한, 당신은 할 수 없습니다. PRAGMA 명령의 문제점은 쿼리 할 수 ​​없다는 것입니다. 이 명령은 SQL 엔진에 데이터를 표시하지 않고 결과를 직접 반환합니다
Kowlown

1
이것은 경쟁 조건을 생성하지 않습니까? 열 이름을 확인하고 열이 누락 된 것을 확인했지만 그 동안 다른 프로세스에서 열을 추가한다고 가정 해 보겠습니다. 그런 다음 열을 추가하려고하지만 이미 존재하기 때문에 오류가 발생합니다. 데이터베이스를 먼저 잠 가야한다고 생각합니까? 나는 sqlite에 대한 멍청이입니다. :).
Ben Farmer

25

DB 업그레이드 문에서이 작업을 수행하는 경우 가장 간단한 방법은 이미 존재할 수있는 필드를 추가하려고 할 때 throw되는 예외를 포착하는 것입니다.

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}

2
저는 예외 스타일 프로그래밍을 좋아하지 않지만 놀랍도록 깔끔합니다. 나를 약간 흔들었을 수도 있습니다.
Stephen J

나도 좋아하지 않지만 C ++는 지금까지 가장 예외적 인 스타일의 프로그래밍 언어입니다. 그래서 나는 그것을 "유효한"것으로 여전히 볼 수 있다고 생각합니다.
tmighty

SQLite의 사용 사례 = 다른 언어 (MSSQL)로 된 어리석은 단순한 일 / 한 줄의 추가 코딩을하고 싶지 않습니다. 좋은 대답입니다 ... "예외 스타일 프로그래밍"이지만 업그레이드 기능에 포함되어 있거나 격리되어 있으므로 허용 가능하다고 생각합니다.
maplemale 2010 년

다른 사람들이 좋아하지 않는 동안, 나는이 롤 최선의 해결책이라고 생각
아담 Varhegyi

13

threre는 PRAGMA의 방법으로 table_info (table_name)이며 테이블의 모든 정보를 반환합니다.

다음은 체크 컬럼 존재 여부에 대한 구현 방법입니다.

    public boolean isColumnExists (String table, String column) {
         boolean isExists = false
         Cursor cursor;
         try {           
            cursor = db.rawQuery("PRAGMA table_info("+ table +")", null);
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndex("name"));
                    if (column.equalsIgnoreCase(name)) {
                        isExists = true;
                        break;
                    }
                }
            }

         } finally {
            if (cursor != null && !cursor.isClose()) 
               cursor.close();
         }
         return isExists;
    }

루프를 사용하지 않고이 쿼리를 사용할 수도 있습니다.

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);

커서 커서 = db.rawQuery ( "select * from tableName", null); 열 = cursor.getColumnNames ();
Vahe Gharibyan

1
나는 :-) 커서를 닫습니다 잊어 생각
Pecana

@VaheGharibyan, 열 이름을 얻기 위해 DB의 모든 것을 선택하기 만하면됩니다! 당신이 단순히 말하는 것은 we give no shit about performance:)).
Farid

마지막 쿼리가 올바르지 않습니다. 적절한 쿼리는 다음 SELECT * FROM pragma_table_info(...)과 같습니다 (pragma와 테이블 정보 사이의 SELECT 및 밑줄에주의). 실제로 어떤 버전에 추가했는지 확실하지 않지만 3.16.0에서는 작동하지 않았지만 3.22.0에서는 작동합니다.
PressingOnAlways

3

pragma table_info()더 큰 SQL의 일부로의 결과 를 사용하려는 경우 .

select count(*) from
pragma_table_info('<table_name>')
where name='<column_name>';

핵심 부분은 사용하는 것 pragma_table_info('<table_name>')대신에 pragma table_info('<table_name>').


이 답변은 @Robert Hawkey의 답변에서 영감을 받았습니다. 내가 새 답변으로 게시하는 이유는 댓글로 게시 할 평판이 충분하지 않기 때문입니다.


1

이 쿼리를 생각해 냈습니다.

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • 내부 쿼리는 열이있는 경우 0 또는 1을 반환합니다.
  • 결과에 따라 열을 변경하십시오.

코드 = 오류 (1), 메시지 = System.Data.SQLite.SQLiteException (0x800007BF) : "ALTER"근처의 SQL 논리 오류 : System.Data.SQLite.SQLite3.Prepare에서 구문 오류가 발생했습니다.
イ ン コ グ ニ ト ア レ ク セ イ


0

위의 답변을 C # /. Net에서 가져 와서 Qt / C ++ 용으로 다시 작성했지만 많이 변경되지는 않았지만 앞으로 C ++의 답을 찾는 모든 사람을 위해 여기에 남겨두고 싶었습니다.

    bool MainWindow::isColumnExisting(QString &table, QString &columnName){

    QSqlQuery q;

    try {
        if(q.exec("PRAGMA table_info("+ table +")"))
            while (q.next()) {
                QString name = q.value("name").toString();     
                if (columnName.toLower() == name.toLower())
                    return true;
            }

    } catch(exception){
        return false;
    }
    return false;
}

0

또는 pragma_table_info와 함께 CASE-WHEN TSQL 문을 사용하여 열이 있는지 확인할 수 있습니다.

select case(CNT) 
    WHEN 0 then printf('not found')
    WHEN 1 then printf('found')
    END
FROM (SELECT COUNT(*) AS CNT FROM pragma_table_info('myTableName') WHERE name='columnToCheck') 

여기에서 테이블을 어떻게 변경합니까? 열 이름이 일치하면?
user2700767

0

여기 내 해결책이 있지만 파이썬에서 (파이썬과 관련된 주제에 대한 게시물을 찾으려고 시도했지만 실패했습니다) :

# modify table for legacy version which did not have leave type and leave time columns of rings3 table.
sql = 'PRAGMA table_info(rings3)' # get table info. returns an array of columns.
result = inquire (sql) # call homemade function to execute the inquiry
if len(result)<= 6: # if there are not enough columns add the leave type and leave time columns
    sql = 'ALTER table rings3 ADD COLUMN leave_type varchar'
    commit(sql) # call homemade function to execute sql
    sql = 'ALTER table rings3 ADD COLUMN leave_time varchar'
    commit(sql)

나는 PRAGMA를 사용하여 테이블 정보를 얻었습니다. 열에 대한 정보로 가득 찬 다차원 배열 (열당 하나의 배열)을 반환합니다. 열 수를 얻기 위해 배열 수를 계산합니다. 열이 충분하지 않으면 ALTER TABLE 명령을 사용하여 열을 추가합니다.


0

한 번에 한 줄씩 실행하면이 모든 대답이 괜찮습니다. 그러나 원래 질문은 단일 db 실행에 의해 실행될 SQL 스크립트를 입력하는 것이었고 모든 솔루션 (예 : 열이 미리 있는지 확인하는 것과 같은)은 실행 프로그램이 어떤 테이블을 알고 있어야하며 열이 변경 / 추가되거나이 정보를 확인하기 위해 입력 스크립트의 사전 처리 및 구문 분석을 수행합니다. 일반적으로 실시간으로 또는 자주 실행하지 않습니다. 그래서 예외를 잡는다는 생각은 받아 들일 수 있고 다음으로 넘어갑니다. 거기에 문제가 있습니다. 운 좋게도 오류 메시지는이를 수행하는 데 필요한 모든 정보를 제공합니다. 아이디어는 alter table 호출에서 예외가 발생하면 sql을 실행하는 것입니다. 우리는 sql에서 alter table 줄을 찾고 나머지 줄을 반환하고 성공하거나 일치하는 alter table 줄을 더 이상 찾을 수 없을 때까지 실행합니다. 다음은 배열에 SQL 스크립트가있는 몇 가지 예제 코드입니다. 각 스크립트를 실행하는 배열을 반복합니다. alter table 명령이 실패하도록 두 번 호출하지만 SQL에서 alter table 명령을 제거하고 업데이트 된 코드를 다시 실행하기 때문에 프로그램이 성공합니다.

#!/bin/sh
# the next line restarts using wish \

exec /opt/usr8.6.3/bin/tclsh8.6  "$0" ${1+"$@"}
foreach pkg {sqlite3 } {
    if { [ catch {package require {*}$pkg } err ] != 0 } {
    puts stderr "Unable to find package $pkg\n$err\n ... adjust your auto_path!";
    }
}
array set sqlArray {
    1 {
    CREATE TABLE IF NOT EXISTS Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      );
    CREATE TABLE IF NOT EXISTS Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        );
    INSERT INTO Version(version) values('1.0');
    }
    2 {
    CREATE TABLE IF NOT EXISTS Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        );
    ALTER TABLE Notes ADD COLUMN dump text;
    INSERT INTO Version(version) values('2.0');
    }
    3 {
    ALTER TABLE Version ADD COLUMN sql text;
    INSERT INTO Version(version) values('3.0');
    }
}

# create db command , use in memory database for demonstration purposes
sqlite3 db :memory:

proc createSchema { sqlArray } {
    upvar $sqlArray sql
    # execute each sql script in order 
    foreach version [lsort -integer [array names sql ] ] {
    set cmd $sql($version)
    set ok 0
    while { !$ok && [string length $cmd ] } {  
        try {
        db eval $cmd
        set ok 1  ;   # it succeeded if we get here
        } on error { err backtrace } {
        if { [regexp {duplicate column name: ([a-zA-Z0-9])} [string trim $err ] match columnname ] } {
            puts "Error:  $err ... trying again" 
            set cmd [removeAlterTable $cmd $columnname ]
        } else {
            throw DBERROR "$err\n$backtrace"
        }
        }
    }
    }
}
# return sqltext with alter table command with column name removed
# if no matching alter table line found or result is no lines then
# returns ""
proc removeAlterTable { sqltext columnname } {
    set mode skip
    set result [list]
    foreach line [split $sqltext \n ] {
    if { [string first "alter table" [string tolower [string trim $line] ] ] >= 0 } {
        if { [string first $columnname $line ] } {
        set mode add
        continue;
        }
    }
    if { $mode eq "add" } {
        lappend result $line
    }
    }
    if { $mode eq "skip" } {
    puts stderr "Unable to find matching alter table line"
    return ""
    } elseif { [llength $result ] }  { 
    return [ join $result \n ]
    } else {
    return ""
    }
}
               
proc printSchema { } {
    db eval { select * from sqlite_master } x {
    puts "Table: $x(tbl_name)"
    puts "$x(sql)"
    puts "-------------"
    }
}
createSchema sqlArray
printSchema
# run again to see if we get alter table errors 
createSchema sqlArray
printSchema

예상 출력

Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------
Error:  duplicate column name: dump ... trying again
Error:  duplicate column name: sql ... trying again
Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------

0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

논리 : sqlite_master의 sql 열에는 테이블 정의가 포함되어 있으므로 확실히 열 이름이있는 문자열이 포함됩니다.

하위 문자열을 검색 할 때 명백한 한계가 있습니다. 따라서 ColumnName에 훨씬 더 제한적인 하위 문자열을 사용하는 것이 좋습니다. 예를 들어 다음과 같이 사용할 수 있습니다.

select * from sqlite_master where type = 'table' and tbl_name = 'MyTable' and sql like '%`MyColumn` TEXT%'

0

두 가지 쿼리로 해결합니다. 이것은 System.Data.SQLite를 사용하는 Unity3D 스크립트입니다.

IDbCommand command = dbConnection.CreateCommand();
            command.CommandText = @"SELECT count(*) FROM pragma_table_info('Candidat') c WHERE c.name = 'BirthPlace'";
            IDataReader reader = command.ExecuteReader();
            while (reader.Read())
            {
                try
                {
                    if (int.TryParse(reader[0].ToString(), out int result))
                    {
                        if (result == 0)
                        {
                            command = dbConnection.CreateCommand();
                            command.CommandText = @"ALTER TABLE Candidat ADD COLUMN BirthPlace VARCHAR";
                            command.ExecuteNonQuery();
                            command.Dispose();
                        }
                    }
                }
                catch { throw; }
            }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.